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


なぜ ISC-DHCP では無く KEA DHCP なのか?
Tomek Mugalski <tomasz@isc.org> さんのプレゼンから抜粋しつつ、KEA の開発経緯とこれからをご紹介しましょう。
何故新しい DHCP サーバーを開発するのでしょうか? それは、ネットワークを構成するテクノロジ、運用が日々複雑化する中で開発開始から十数年が経つ ISC-DHCP のコード・ベースではネットワークの進化に見合う期間で安全に機能拡張・性能改善していくことが難しいから。
確かに商用製品が ISC-DHCP を比較に出す時には、より高度なリース性能・運用インタフェースを挙げることが多いでしょう。 それらが本当に必要かは利用者が冷静に判断する必要がありますが、数十万〜数百万単位でクライアントを管理する大規模通信事業者にとって色々と不満があったのは事実。
このような状況の中、ISC 社はある決断をします。 2014 年中に BIND10 の開発からは手を引き、KEA (DHCP, DNSサーバー)の開発に注力すると。
KEA の概要はこうです。 IPv6, IPv4 に対応し、大規模ネットワークでも十分に利用できるだけの機能・性能をもった DHCP とダイナミック DNS サーバー(僕は DDNS の方は詳しく見てませんが)である。 尚、perfdhcp はサーバー・サイドのパフォーマンス測定・統計分析ツールであり DHCP クライアントをエミュレーションする負荷生成ツールでは無いようです。
KEA ではストーレジ・エンジンの選択肢が大幅に広がりました。 MySQL、PostgreSQL といった DBMS から高速性だけを追求したインメモリ・モードをサポートします。 また、ストレージング処理は抽象化されており、独自のエンジンを選ぶことも容易になります。 ※性能の大部分はリース状態の管理(空きアドレスの検索、リース期間のハンドリングなど)が占めます。
さて、最後にロードマップを紹介しましょう。
この記事を書いている今日は 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 が正式にサポートする OS(ビルドと試験が行われているサーバー。UNIX 及び ライクな OS の多くでビルドまでの確認が行われており動作に問題は無いだろう、とのこと)は RedHat Enterprise Linux, CentOS, Fedora and FreeBSD なのですが、今日は CentOS 6 を使ってセットアップしてみますよ。
まずはビルド環境の構築から。
log4cplus は yum リポジトリには見つからないのでソースからビルド。
あともう一つ、 データの暗号化に利用する Botan(http://botan.randombit.net)もソースコードからビルドしましょう。 脆弱な開発体制を起因としたセキュリティ・ショックで何かと話題になる OpenSSL もサポートしているのですが、Botan が見つかれば優先してリンクします。
KEA は開発版では無くリリース版(https://www.isc.org/downloads/)の中で最も新しい 0.9 を使い、MySQL を有効にしてビルド、インストールします。
あと、もう少し。 僕は MySQL を使うので データーベースを作成して、
データベースとテーブルが作成されていればOK。
僕と同じ方法ならばインストール先 prefix は /usr/local になり /usr/local/share/doc/kea/examples の下に IPv4, IPv6 のサンプル設定ファイルがあるはずです。 ここでは IPv4 を中心にリレーエージェントを経由する DHCP ネットワーク、というモデルで考えます。

ここまで来たら 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 にして下さい。
実行してみると・・・
お、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 にもリース情報が保存されているはずですよ。
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 ネットワーク、というモデルで考えます。

例えば 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 しないバギーなクライアントとそのデバイスを使う何の責任も無い・困り果てたユーザーがいたその時に助けてあげることができませんからね。
しかし、このモードだけではサンプル・コードで補足したように、正しい server id 値を DHCP REQUEST しないバギーなクライアントとそのデバイスを使う何の責任も無い・困り果てたユーザーがいたその時に助けてあげることができませんからね。
#僕は現実的・寛容なタイプの人間なのです。
Linux高信頼サーバ構築ガイド クラスタリング編 (Industrial Computing Series)
posted with amazlet at 15.01.23
笠野 英松
CQ出版
売り上げランキング: 467,996
CQ出版
売り上げランキング: 467,996
NETGEAR Inc. GS116E 【ライフタイム保証】 16ポート ギガビット アンマネージプラススイッチ GS116E-200JPS
posted with amazlet at 15.01.23
ネットギア (2013-11-18)
売り上げランキング: 2,581
売り上げランキング: 2,581