この記事は「Go で作るモダン・ブートサーバー」の 3 日目の記事です。 今回からはお待ちかねの、実装が始まります。 この回では Go で DHCP サーバーを実装します。
シリーズの記事は以下のリンクからどうぞ。
- Go で作るモダン・ブートサーバー Day 1 - 基礎知識編
- Go で作るモダン・ブートサーバー Day 2 - 環境構築編
- Go で作るモダン・ブートサーバー Day 3 - 雑 DHCP サーバー実装編 ← この記事
- Go で作るモダン・ブートサーバー Day 4 - PXE サーバー実装編
- Go で作るモダン・ブートサーバー Day 5 - iPXE サーバー実装編
今回は DHCP サーバーを実装できればと思い記事を書きましたが、仕様の説明だけで長くなり(仕様も全部説明できなかった)、きちんとした DHCP サーバーの実装まで至りませんでした。 そのため DHCP サーバーの実装も数段階に分けて進めたいと思います。 この記事では DHCP の仕様を説明しつつ、固定 IP アドレスを割り当てるガバガバ DHCP サーバーを実装します。
今回作成するコードは以下のレポジトリで公開しています。
DHCP (Dynamic Host Configuration Protocol) について
DHCP (RFC 2131)は、クライアントが IP アドレスやネットマスクなどの設定をネットワーク経由で自動的に取得するプロトコルです。 DHCP は BOOTP (Bootstrap Protocol, RFC 951) を拡張したプロトコルで、IP アドレスのリース機能と追加の設定項目が利用可能になりました。 現在では DHCP はネットワーク接続時の自動設定という印象が強いですが、名前にあるとおり BOOTP 自体はマシンのブートストラップを想定したプロトコルです。
DHCP メッセージ
DHCP のメッセージにはいくつかの種類があり、RFC 2131 では 8 つの DHCP メッセージが定義されています。 このうち DHCP サーバーがクライアントに IP アドレスを割り当てる時に利用されるのは、DHCPDISCOVER、DHCPOFFER、DHCPREQUEST、DHCPACK の 4 つのメッセージです。 次の図は DHCP サーバーが IP アドレスをクライアントに割り当てるまでにやり取りするメッセージです。
- DHCPDISCOVER: クライアントが DHCP サーバーを探すために、L2 ネットワーク内にブロードキャストします。
- DHCPOFFER: DHCPDISCOVER を受け取った DHCP サーバーが、設定値をクライアントに返答します。
- DHCPREQUEST: DHCPOFFER メッセージを受け取ったクライアントが、DHCP サーバーに返答します。
- DHCPACK: DHCPREQUEST を受け取った DHCP サーバーが、クライアントに設定値を使ってよいという返答します。
パケット構造
DHCP は UDP のプロトコルです。 DHCP で送受信するパケット構造は、DHCP サーバーが送信するパケットと DHCP クライアントが送信するパケットで同じです。 それぞれのメッセージの種類ごとに、各フィールドに適切な値を埋めます。 以下が DHCP のパケット構造です。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| op (1) | htype (1) | hlen (1) | hops (1) |
+---------------+---------------+---------------+---------------+
| xid (4) |
+-------------------------------+-------------------------------+
| secs (2) | flags (2) |
+-------------------------------+-------------------------------+
| ciaddr (4) |
+---------------------------------------------------------------+
| yiaddr (4) |
+---------------------------------------------------------------+
| siaddr (4) |
+---------------------------------------------------------------+
| giaddr (4) |
+---------------------------------------------------------------+
| |
| chaddr (16) |
| |
| |
+---------------------------------------------------------------+
| |
| sname (64) |
+---------------------------------------------------------------+
| |
| file (128) |
+---------------------------------------------------------------+
| |
| options (variable) |
+---------------------------------------------------------------+
DHCP サーバーからの返答メッセージのフィールド
DHCP サーバーがクライアントに送信する DHCPOFFER と DHCPACK メッセージでは、それぞれのフィールドの値は以下のとおりです。 DHCP サーバーはこの仕様に従い、それぞれのパケットを埋めます。
フィールド | DHCPOFFER | DHCPACK | フィールドの説明 |
---|---|---|---|
op | BOOTREPLY (2) | BOOTREPLY (2) | DHCP サーバーへの要求が BOOTREQUEST(1)、DHCP からの応答が BOOTREPLY (2) |
htype | 1 | 1 | ハードウェアのタイプ。イーサネットは 1 |
hlen | 5 | 6 | MAC アドレスの長さ。イーサネットは 1 |
hops | 0 | 0 | DHCP リレーエージェントのホップ数 |
xid | DHCPDISCOVER の xid | DHCPREQUEST の xid | クライアントが生成する一貫した ID(ランダム) |
secs | 0 | 0 | クライアントのリクエストが開始してからの経過秒数 |
ciaddr | 0 | DHCPREQUEST の ciaddr | クライアントの IP アドレス |
yiaddr | 割り当てアドレス | 割り当てアドレス | クライアントに割り当てる IP アドレス |
siaddr | ブートサーバーのアドレス | ブートサーバーのアドレス | ブートサーバーのアドレス |
flags | DHCPDISCOVER の flags | DHCPREQUEST の flags | ブロードキャストかユニキャストかを表す |
giaddr | DHCPDISCOVER の giaddr | DHCPREQUEST の giaddr | リレーエージェントの IP アドレス |
chaddr | DHCPDISCOVER の chaddr | DHCPREQUEST の chaddr | クライアントの MAC アドレス |
sname | サーバー名 | サーバー名 | DHCP サーバーのサーバー名 |
file | ブートファイル | ブートファイル | ブートに必要なブートサーバー上のファイル名 (TFTP のパス名や HTTP ブートの URL) です。 |
options | options | options | オプション |
options は予約された番号と対応する数値や文字列、IP アドレスを載せられるフィールドです。
例えば 54
はサーバー識別子を表す番号で、その値は DHCP サーバーの IP アドレスを載せられます。
この番号とその意味は RFC 2132 で定義されています。
DHCP サーバーの実装
さて、上記の仕様に基づいた、IP アドレスの割り当てを実装してみましょう。 DHCP サーバーが受け取るのは DHCPDISCOVER と DHCPREQUEST で、それぞれのメッセージで DHCPOFFER と DHCPACK を返します。
上記のとおりに UDP パケットを組み立てても良いのですが(Go なら簡単ですよね)、 もっと簡単に実装するために go.universe.tf/netboot パッケージを使います。 このパッケージは DHCP4 や DHCP6、そして TFTP のライブラリを含みます。
サーバーを建てる
それでは DHCPDISCOVER を受け取った時に DHCPOFFER を返し、DHCPREQUEST を受け取った時に DHCPACK を返す簡単なサーバーを作ります。 go.universe.tf/netboot の dhcp4 パッケージでは、op、htype、hlen、hops は自動で埋めてくれます。 また何も設定しなければゼロ値が使われるので、上記表の 0 の部分は何も代入しなくて良いです。
固定アドレを割り当てる DHCP サーバーの実装が以下のとおりです。 ほんの 50 行ほどでできました。
package main
import (
"log"
"net"
"go.universe.tf/netboot/dhcp4"
)
func main() {
listen := "0.0.0.0:67"
conn, err := dhcp4.NewConn(listen)
if err != nil {
log.Fatalf("[FATAL] Unable to listen on %s: %v", listen, err)
}
defer conn.Close()
log.Printf("[INFO] Starting DHCP server...")
for {
req, intf, err := conn.RecvDHCP() // (1)
if err != nil {
log.Fatalf("[ERROR] Failed to receive DHCP package: %v", err)
}
log.Printf("[INFO] Received %s from %s", req.Type, req.HardwareAddr)
resp := &dhcp4.Packet{ // (2)
TransactionID: req.TransactionID,
HardwareAddr: req.HardwareAddr,
ClientAddr: req.ClientAddr,
YourAddr: net.IPv4(172, 24, 32, 1),
Options: make(dhcp4.Options),
}
resp.Options[dhcp4.OptSubnetMask] = net.IPv4Mask(255, 255, 0, 0)
switch req.Type { // (3)
case dhcp4.MsgDiscover:
resp.Type = dhcp4.MsgOffer
case dhcp4.MsgRequest:
resp.Type = dhcp4.MsgAck
default:
log.Printf("[WARN] message type %s not supported", req.Type)
continue
}
log.Printf("[INFO] Sending %s to %s", resp.Type, resp.HardwareAddr)
err = conn.SendDHCP(resp, intf) // (4)
if err != nil {
log.Printf("[ERROR] unable to send DHCP packet: %v", err)
}
}
}
- パケットを受け取るとこのメソッドは処理を返します。返り値にパケットと、パケットを受け取ったインターフェイスが返ります。
- それぞれのメッセージで共通するフィールドをあらかじめ設定します。
xid (
TransactionID
) 、chaddr (HardwareAddr
)、ciaddr (ClientAddr
) はリクエストの値を使います。 yiaddr (YourAddr
) とサブネットマスクは、固定で172.24.32.1
と255.255.0.0
を設定します。 - リクエストのメッセージタイプごとに、クライアントに返すメッセージタイプを設定します。
- 作成したパケットを返信します。
検証
それでは実装した DHCP サーバーが正常に動くか試してみましょう。 検証は前回の記事で作成した環境を使用します。 環境を作成はシェルスクリプトにまとめてあります。
まずは仮想ネットワークを作成します。
sudo ./bin/setup network --name br0 --address 172.24.0.1/16
そして上記の Go ファイルをビルドして起動します。 67 番ポートに LISTEN するには管理者権限が必要なので sudo で実行します。
go build . && sudo ./building-boot-server
そしてクライアントの VM を起動します。
sudo ./bin/setup node --network br0
すると、DHCP サーバーのログにパケットの受け渡しの様子が観測できます。
2020/03/18 21:55:22 [INFO] Starting DHCP server...
2020/03/18 21:56:37 [INFO] Received DHCPDISCOVER from 52:54:00:96:e6:c3
2020/03/18 21:56:37 [INFO] Sending DHCPOFFER to 52:54:00:96:e6:c3
2020/03/18 21:56:38 [INFO] Received DHCPDISCOVER from 52:54:00:96:e6:c3
2020/03/18 21:56:38 [INFO] Sending DHCPOFFER to 52:54:00:96:e6:c3
2020/03/18 21:56:40 [INFO] Received DHCPREQUEST from 52:54:00:96:e6:c3
2020/03/18 21:56:40 [INFO] Sending DHCPACK to 52:54:00:96:e6:c3
そしてクライアントの起動メッセージを見てみましょう。
まず起動時に NIC の MAC アドレス(この場合 52:54:00:96:e6:c3
)が表示されます。
この値が DHCP サーバーのログと一致することが確認できます。
そして DHCP サーバーが割り当てた(固定の)IP アドレス 172.24.32.1/16
が、クライアントに設定されていることが分かります。
無事 DHCP サーバーが動き、クライアントに IP アドレスを割り当てることができました!
おわりに
今回作成したコードは以下のレポジトリで公開しています。
DHCP といえばなんとなくネットワークの設定をするものだとイメージしがちですが、仕様を調べてみると面白いです。 今回実装した DHCP サーバーは固定 IP を割り当てるガバガバ実装ですが、この状態でブート情報を渡すときちんとブートストラップが開始します。
次回以降でもう少しマシな DHCP サーバーを実装していこうと思います。 それでは次回もお楽しみに!