2015年01月23日

ISC、dhcpd やめるってよ - 次世代 ISC DHCP サーバー Kea 導入・設定ガイド

僕らが普段インターネットへ接続するとき IP アドレスを自動取得するのに必ずと言ってよいほどお世話になっている DHCP サーバー。 オープン・ソースかつ大規模構成でも利用可能といえば ISC(Internet Systems Consortium)社の開発する ISC-DHCP なのですが、今後数年で開発を中止し、Kea に切り替わるという話があるのをご存知ですか? 今日は大規模なネットワークの構築・運用に関わる技術者であればそろそろ抑えておきたい KEA DHCP サーバーを動かしてみますよ。

zxm500

なぜ ISC-DHCP では無く KEA DHCP なのか?


Tomek Mugalski <tomasz@isc.org> さんのプレゼンから抜粋しつつ、KEA の開発経緯とこれからをご紹介しましょう。

何故新しい DHCP サーバーを開発するのでしょうか? それは、ネットワークを構成するテクノロジ、運用が日々複雑化する中で開発開始から十数年が経つ ISC-DHCP のコード・ベースではネットワークの進化に見合う期間で安全に機能拡張・性能改善していくことが難しいから。

kea_why_new_dhcp_implementation

確かに商用製品が ISC-DHCP を比較に出す時には、より高度なリース性能・運用インタフェースを挙げることが多いでしょう。 それらが本当に必要かは利用者が冷静に判断する必要がありますが、数十万〜数百万単位でクライアントを管理する大規模通信事業者にとって色々と不満があったのは事実。

このような状況の中、ISC 社はある決断をします。 2014 年中に BIND10 の開発からは手を引き、KEA (DHCP, DNSサーバー)の開発に注力すると。

kea_and_bind_future

KEA の概要はこうです。 IPv6, IPv4 に対応し、大規模ネットワークでも十分に利用できるだけの機能・性能をもった DHCP とダイナミック DNS サーバー(僕は DDNS の方は詳しく見てませんが)である。 尚、perfdhcp はサーバー・サイドのパフォーマンス測定・統計分析ツールであり DHCP クライアントをエミュレーションする負荷生成ツールでは無いようです。

kea_overview

KEA ではストーレジ・エンジンの選択肢が大幅に広がりました。 MySQL、PostgreSQL といった DBMS から高速性だけを追求したインメモリ・モードをサポートします。 また、ストレージング処理は抽象化されており、独自のエンジンを選ぶことも容易になります。 ※性能の大部分はリース状態の管理(空きアドレスの検索、リース期間のハンドリングなど)が占めます。

kea_lease_storage

さて、最後にロードマップを紹介しましょう。

kea_roadmap

この記事を書いている今日は 2015/Q1 に相当し予定通りバージョンは 0.9 がリリースされいてます。 今後ですが、ISC-DHCP で class, group に相当するクライアント管理、固定アドレスなどの最低限の管理機能を実装したバージョン 1.0 が今年の4月にリリースされる予定です。 個人的には Remote Management Interface、configuration scaling (support 1000s of subnets) が整備されるバージョン1.1あたりで真剣に評価する必要があるのかな、と考えています(より詳しい情報は http://kea.isc.org をどうぞ)。


KEA DHCP バージョン 0.9 のインストール


それでは、お先に失礼させて頂きます。 KEA が正式にサポートする OS(ビルドと試験が行われているサーバー。UNIX 及び ライクな OS の多くでビルドまでの確認が行われており動作に問題は無いだろう、とのこと)は RedHat Enterprise Linux, CentOS, Fedora and FreeBSD なのですが、今日は CentOS 6 を使ってセットアップしてみますよ。

まずはビルド環境の構築から。 

$ sudo yum install gcc-c++
$ sudo yum install mysql-server mysql-devel
$ sudo yum install boost boost-devel


log4cplus は yum リポジトリには見つからないのでソースからビルド。

$ wget http://downloads.sourceforge.net/project/log4cplus/log4cplus-stable/1.1.3/log4cplus-1.1.3-rc4.tar.gz
$ tar xvzf log4cplus-1.1.3-rc4.tar.gz
$ cd log4cplus-1.1.3-rc4
$ ./configure
$ make
$ sudo make install


あともう一つ、 データの暗号化に利用する Botan(http://botan.randombit.net)もソースコードからビルドしましょう。 脆弱な開発体制を起因としたセキュリティ・ショックで何かと話題になる OpenSSL もサポートしているのですが、Botan が見つかれば優先してリンクします。

$ wget http://botan.randombit.net/releases/Botan-1.10.9.tgz
$ tar xvzf Botan-1.10.9.tgz
$ cd Botan-1.10.9
$ ./configure.py
$ make
$ sudo make install


KEA は開発版では無くリリース版(https://www.isc.org/downloads/)の中で最も新しい 0.9 を使い、MySQL を有効にしてビルド、インストールします。

$ tar xvzf kea-0.9.tar.gz
$ cd kea-0.9
$ ./configure --with-dhcp-mysql
...
       Kea source configure results:
    -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

Package:
  Name:            kea
  Version:         0.9

Python3:
  not installed

Boost:
  BOOST_VERSION:   1.41

Botan:
  CRYPTO_VERSION:  1.10.9

OpenSSL: no

Log4cplus:
  LOG4CPLUS_VERSION: 1.1.3

Kea config backend:
  CONFIG_BACKEND:  JSON

MySQL:
  MYSQL_VERSION:   5.1.73

PostgreSQL:
  no

Features:

Developer:
  Enable Debugging: no
  Google Tests: no
  Valgrind: not found
  C++ Code Coverage: no
  Python Code Coverage:
  Logger checks: no
  Generate Documentation: no

$ make
$ sudo make install


あと、もう少し。 僕は MySQL を使うので データーベースを作成して、

$ sudo /etc/init.d/mysqld start
$ sudo chkconfig mysqld on
$ mysql -uroot -p -e "create database kea"


ここに KEA に付属する DDL スクリプトをインポート。 KEA に設定する DB ユーザーも作ります(ここでは keadbuser, keadbpasswd)。

# kea 0.9.1 path: /usr/local/share/kea/scripts/mysql/dhcpdb_create.mysql
$ mysql -uroot -p kea < /usr/local/share/kea/dhcpdb_create.mysql $ mysql -uroot -p -e "create user 'keadbuser'@'localhost' IDENTIFIED BY 'keadbpasswd'" $ mysql -uroot -p -e "grant all privileges on kea.* to keadbuser@localhost identified by 'keadbpasswd';flush privileges;"


データベースとテーブルが作成されていればOK。

$ mysql -u keadbuser -p kea
Enter password:

mysql> show tables;
+----------------+
| Tables_in_kea  |
+----------------+
| lease4         |
| lease6         |
| lease6_types   |
| schema_version |
+----------------+
4 rows in set (0.00 sec)


mysql> desc lease4;
+----------------+------------------+------+-----+-------------------+-----------------------------+
| Field          | Type             | Null | Key | Default           | Extra                       |
+----------------+------------------+------+-----+-------------------+-----------------------------+
| address        | int(10) unsigned | NO   | PRI | NULL              |                             |
| hwaddr         | varbinary(20)    | YES  | MUL | NULL              |                             |
| client_id      | varbinary(128)   | YES  | MUL | NULL              |                             |
| valid_lifetime | int(10) unsigned | YES  |     | NULL              |                             |
| expire         | timestamp        | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
| subnet_id      | int(10) unsigned | YES  |     | NULL              |                             |
| fqdn_fwd       | tinyint(1)       | YES  |     | NULL              |                             |
| fqdn_rev       | tinyint(1)       | YES  |     | NULL              |                             |
| hostname       | varchar(255)     | YES  |     | NULL              |                             |
+----------------+------------------+------+-----+-------------------+-----------------------------+
9 rows in set (0.00 sec)

mysql> quit;


KEA DHCP バージョン 0.9 でのコンフィグレーション


僕と同じ方法ならばインストール先 prefix は /usr/local になり /usr/local/share/doc/kea/examples の下に IPv4, IPv6 のサンプル設定ファイルがあるはずです。 ここでは IPv4 を中心にリレーエージェントを経由する DHCP ネットワーク、というモデルで考えます。

kea dhcp network model
例えば ISC-DHCP サーバーを使っていて、こんな設定(dhcpd.conf)だったとします。

#not authoritative;
log-facility local*; # your log facility
ddns-update-style none;
ping-check true;

default-lease-time 86400;

# direct connected network.
subnet 172.16.0.0 netmask 255.255.0.0 {
}

# managed network.
subnet 192.0.0.0 netmask 255.255.0.0 {
  option routers 192.0.0.1;
  option domain-name-servers 8.8.8.8,8.8.4.4;
  pool {
    range 192.0.1.1 192.0.255.1;
  }
}


これを KEA で書くとこんなふうになります(設定ファイルは JSON 形式。 IPv6 は適当)。 コメント行がインデントされていないのはバージョン 0.9 のパーサーではエラーになるから。 ピキピキ(#^ω^)

{
# DHCPv4
  "Dhcp4": {
# kea 0.9.1: unsupported global configuration parameter: interface. you must use interfaces-config. "interfaces": [ "eth0" ], # store leases to db "lease-database": { "type": "mysql", "name": "kea", "user": "keadbuser", "host": "localhost", "password": "keadbpasswd" }, # store leases to memfile # "lease-database": { # "type": "memfile" # }, # lease-lifetime (mandaory) # Addresses will be assigned with valid lifetimes being 4000. Client # is told to start renewing after 1000 seconds. If the server does not respond # after 2000 seconds since the lease was granted, client is supposed # to start REBIND procedure (emergency renewal that allows switching # to a different server). valid-lifetime paramerter is mandatory. "valid-lifetime": 86400, # Renew and rebind timers are commented out. This implies that options # 58 and 59 will not be sent to the client. In this case it is up to # the client to pick the timer values according to RFC2131. Uncomment the # timers to send these options to the client. # "renew-timer": 1000, # "rebind-timer": 2000, # Server parameters. # http://kea.isc.org/wiki/ConfigurationMigration # "ping-check": true, # "max-lease-time": 86400, # not supported at 0.9. # "default-lease-time": 86400, # not supported at 0.9 # ddns-update-style is N/A (default disable). "subnet4": [{ # DHCP options "option-data": [ { "name": "routers", # http://kea.isc.org/docs/kea-guide.html # - Table 6.1. List of standard DHCPv4 options "code": 3, "space": "dhcp4", # If csv-format is set to false, # option data must be specified as a hexadecimal string. "csv-format": true, "data": "192.0.0.1" }, { "name": "domain-name-servers", "code": 6, "space": "dhcp4", "csv-format": true, "data": "8.8.8.8, 8.8.4.4" } # ,{ # "name": "dhcp-lease-time", # "code": 51, # "space": "dhcp4", # "csv-format": true, # "data": "86400" # } ], "pools": [ { "pool": "192.0.1.1 - 192.0.255.1" } ], "subnet": "192.0.0.0/16" }, { "subnet": "172.16.0.0/24" }] }, # DHCPv4 specific configuration ends here. # DHCPv6 "Dhcp6": { "interfaces": [ "eth0" ], "preferred-lifetime": 3000, "valid-lifetime": 4000, "renew-timer": 1000, "rebind-timer": 2000, "subnet6": [{ # "pools": [{ "pool": "2001:db8:1::/80"}], "subnet": "2001:db8:1::/64" }] }, # DHCPv6 specific configuration ends here. # Logger "Logging": { "loggers": [{ "name": "*", "severity": "DEBUG" }] } # Logger parameters end here. }


IPv4、IPv6 でブロックが別れ、馴染み深い max-lease-time や ping-check など幾つかのパラメーターが「現時点では」使えないことがわかります(最新の状況は http://kea.isc.org/wiki/ConfigurationMigration で確認可能)。

dhcp オプションの定義方法も変わりましたよね。 コメント・アウトされている DHCP オプション 51(dhcp-lease-time)は試しに設定してみたのですが、やはり valid-lifetime じゃなきゃダメでした。 詳しい設定方法は管理者ガイドをどうぞ(http://ftp.isc.org/isc/kea/0.9/doc/kea-guide.html ← バージョン番号が URL に含まれることに注意)。

サーバーの起動方法は keactrl (start | stop | status) か利用する DHCP バージョンごとにサーバーの実体である kea-dhcp4, kea-dhcp6 を使うかの大きく2つ。 尚、keactrl を使った場合でも /usr/local/etc/kea/keactrl.conf で利用するバージョンを定義できます。

さて、サービスを起動してみましょう。 標準では /usr/local/etc/kea/kea.conf がデフォルトの設定ファイルになりますがここでは個別にパスを指定(-c)し、フォア・グラウンドで起動しています。

$ sudo /usr/local/sbin/kea-dhcp4 -c /path/to/kea-dhcpd.json
2015-01-22 INFO  [kea-dhcp4.dhcp4] DHCP4_STARTING Kea DHCPv4 server version 0.9 starting
2015-01-22 INFO  [kea-dhcp4.dhcpsrv] DHCPSRV_CFGMGR_ADD_IFACE listening on interface eth1
2015-01-22 INFO  [kea-dhcp4.dhcpsrv] DHCPSRV_MYSQL_DB opening MySQL lease database.
2015-01-22 INFO  [kea-dhcp4.dhcp4] DHCP4_CONFIG_NEW_SUBNET a new subnet has been added to configuration: 192.0.0.0/16 with params: valid-lifetime=86400
2015-01-22 INFO  [kea-dhcp4.dhcp4] DHCP4_CONFIG_NEW_SUBNET a new subnet has been added to configuration: 172.16.0.0/24 with params: valid-lifetime=86400
2015-01-22 INFO  [kea-dhcp4.dhcp4] DHCP4_CONFIG_COMPLETE DHCPv4 server has completed configuration: added IPv4 subnets: 2; DDNS: disabled


ここまで来たら DHCP ACK まで見たいですよね! 同じサーバーに scapy ( http://www.secdev.org/projects/scapy/)をインストール、Python で DHCP クライアントをエミュレートしてみましょう。

このスクリプトは giaddr で指定したアドレスを持つリレーエージェントとして動き、引数に指定した数(未指定は1クライアント)だけクライアントを生成して DHCP トランザクション(DHCP DISCOVER - DHCP ACK or NAK)の成立を試みます。 Kea とは別のサーバーで動かすのであれば 192.0.0.0/16 への GW をそのサーバーの IP にして下さい。

#!/usr/bin/env python

from scapy.all import *
import threading
import time
import datetime
import fpformat
import signal
import binascii

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 = 1
num_clients = NUM_MAX_CLIENT

if num_argvs > 1:
        num_clients = int(argv[1])

conf.iface = "eth0"
#conf.iface = "eth1"
#conf.iface = "vboxnet1"

# op82 = Agent Circuit ID (id, len, value) + Agent Remote ID (id, len, value)
op82 = "\x01\x01\x01" + "\x02\x06\x4e\xca\x02\xe1\xf1\x61"

giaddr = "192.0.0.1" # relay agent addr

class DHCPDHandler(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self) 

    def callbak(self, pkt):
        global giaddr
        if DHCP in pkt:
            # debug
            #pkt.show()

            # DHCP options 
            # [('subnet_mask', '*'), ('router', '*'), ('message-type', *), ('server_id', '*'),..., 'end']
            options = pkt[DHCP].options
            #print options
            mtype = None
            server_id = None
            for op in options:
                if type(op) is tuple:
                    if op[0] == "message-type":
                         mtype = op[1]
                    elif op[0] == "server_id":
                         server_id = op[1]

            yipaddr = pkt[BOOTP].yiaddr
            hexes = binascii.hexlify(pkt[BOOTP].chaddr)[0:12]  
            human_chaddr = ':'.join(hexes[x:x+2] for x in xrange(0, len(hexes), 2))
            if mtype == MESSAGE_TYPE_OFFER:
                print 'DHCPOFFER:\t %s (%s) from %s' % (yipaddr, human_chaddr, pkt[IP].src)
                request = (
                    Ether(src=pkt.dst, 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, hops=2)/
                    DHCP(
                        options=[
                            ('message-type', 'request'),
                            ('requested_addr', yipaddr),
                            ('server_id', server_id),
                            ('relay_agent_Information', op82),
                            ('vendor_class_id', 'scapy2.0'),
                            ('end')]
                    )
                )
                print 'DHCPREQUEST:\t %s (%s) to %s' % (yipaddr, human_chaddr, server_id)
                #request.show()
                sendp(request, verbose=0)

            elif mtype == MESSAGE_TYPE_ACK:
                #pkt.show()
                print 'DHCPACK:\t %s (%s) from %s' % (yipaddr, human_chaddr, pkt[IP].src)
                print "        options: " + str(options)

            elif mtype == MESSAGE_TYPE_NAK:
                print 'DHCPNAK:\t %s (%s) from %s' % (yipaddr, human_chaddr, pkt[IP].src)

            elif mtype != MESSAGE_TYPE_DISCOVER and mtype != MESSAGE_TYPE_REQUEST:
                print 'UNKNOWN:\t %s (%s) from %s' % (mtype, human_chaddr, pkt[IP].src)

    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):
        for discover in self.discovers:
            print 'DHCPDISCOVER:\t %s relay with %s' % (discover.src, giaddr)
            sendp(discover, verbose=0) # Send packet at layer 2
            #time.sleep(0.1)

# create dhcp discover packets as relay.
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,hops=2)/
            DHCP(
                options=[
                    ('message-type','discover'),
                    ('relay_agent_Information', op82),
                    ('vendor_class_id', 'scapy2.0'),
                    ('end')])
            )
    discovers.append(discover)
    print "client-%s %s" % (xid, mac)
    xid = xid + 1


def handle_SIGINT(signal, frame) :
    print "Shutting down..."
    sys.exit(0)

signal.signal(signal.SIGINT, handle_SIGINT)

dh = DHCPDHandler()
dh.daemon = True
dh.start()
time.sleep(1)

ds = DHCPStormer(discovers)
ds.daemon = True
ds.start()

while 1:
    time.sleep(1)


実行してみると・・・

$ sudo python dhcp-relay-agent.py
client-1 94:35:6e:17:20:b9
DHCPDISCOVER:    94:35:6e:17:20:b9 relay with 192.0.0.1
DHCPOFFER:       192.0.1.16 (94:35:6e:17:20:b9) from 172.16.0.1
DHCPREQUEST:     192.0.1.16 (94:35:6e:17:20:b9) to 172.16.0.1
DHCPACK:         192.0.1.16 (94:35:6e:17:20:b9) from 172.16.0.1
    options: [('subnet_mask', '255.255.0.0'), ('router', '192.0.0.1'), ('name_server', '8.8.8.8', '8.8.4.4'), ('lease_time', 86400), ('message-type', 5), ('server_id', '172.16.0.1'), ('relay_agent_Information', '\x01\x01\x01\x02\x05scapy'), 'end']


お、DHCP ACK まで成立したみたい。 giaddr のネットワークが Kea の subnet に定義されていないと OFFER しませんし、Python コードを変更する場合、OFFER パケット/DHCP オプション 54 番(server id)で示唆される DHCP サーバーの IP アドレスを消してしまうと信頼されないネットワークとして NAK が戻るので注意して下さいね。
# hops を 0 にすると DHCP4_NO_SUBNET_FOR_DIRECT_CLIENT エラーが出ます(ISC-DHCP では誤魔化せたのに・・・こいつ、賢くなってる!)。

DB にもリース情報が保存されているはずですよ。

mysql> select count(*) from lease4;
+----------+
| count(*) |
+----------+
|      307 |
+----------+
1 row in set (0.00 sec)


ホーム、小規模なオフィス・ネットワーク用であればもう試しても良いぐらいのデキ。 少し気になったのは ISC-DHCP でいう authoritative で動き、not authoritative にする方法が現時点(v 0.9)ではわからなかったところ。 authoritative は設定ファイルが正しい、つまり絶対的に正しい知識を持つネットワーク管理者によって設定されたものであり、これにそぐわないクライアントからリクエストを受信したならば拒否通知(DHCP NAK)する、という厳格な動作になります。

しかし、このモードだけではサンプル・コードで補足したように、正しい server id 値を DHCP REQUEST しないバギーなクライアントとそのデバイスを使う何の責任も無い・困り果てたユーザーがいたその時に助けてあげることができませんからね。

#僕は現実的・寛容なタイプの人間なのです。



Posted by netbuffalo at 21:00│Comments(0)TrackBack(0)ユーティリティ | ネットワーク


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

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

コメントする

名前
URL
 
  絵文字