この記事は「Go で作るモダン・ブートサーバー」の 4 日目の記事です。 この記事では PXE ブートができるブートサーバーの実装をします。
シリーズの記事は以下のリンクからどうぞ。
- Go で作るモダン・ブートサーバー Day 1 - 基礎知識編
- Go で作るモダン・ブートサーバー Day 2 - 環境構築編
- Go で作るモダン・ブートサーバー Day 3 - 雑 DHCP サーバー実装編
- Go で作るモダン・ブートサーバー Day 4 - PXE サーバー実装編 ← この記事
- Go で作るモダン・ブートサーバー Day 5 - iPXE サーバー実装編
PXE ブートは古くからあるネットワークブートの規格の 1 つです。 この記事では前回実装した DHCP サーバーを元に、新たに TFTP サーバーを実装します。 そして PXE ブート環境を構築して、実際に Linux がネットワーク経由でブートするまでを実装します。 今回作成するコードは以下のレポジトリで公開しています。
PXE ブート
PXE ブートは古くからあるネットワークブートの規格の 1 つです。 PXE ブートは DHCP と TFTP を組み合わせたブート方式です。 PXE ブートはブートするクライアントマシンが持つ MAC アドレスや UUID に基づいた自動設定もできるよう設計されています。
TFTP (Trivial File Transfer Protocol)
TFTP (RFC 1350) は UDP ベースのファイル転送プロトコルです。 FTP と比較するとシンプルなプロトコルで、認証やアクセス制限などの機能はありません。 TFTP クライアントは実装が容易で使用リソースも少なく済むので、ブートストラップで利用されたり、リソースが限られた組み込み機器の起動にも利用されます。 セキュリティ的な機能は無いため、極秘ファイルの転送や改ざんされると困る状況では利用できません。
DHCP メッセージのブートオプション
PXE ブートでは、DHCP サーバーが応答メッセージの siaddr
、 file
フィールドにブート情報を返します。
siaddr
と file
フィールドは、それぞれ TFTP サーバーのアドレスとパスです
(その他の DHCP メッセージ上のフィールドについては RFC 2131、または前回の記事を参照)。
PXE クライアントは上記のフィールド情報をもとに、TFTP サーバーから実行ファイルをロードします。 PXE ブートではまず最初にブートローダーを起動するのが慣例です。 今回は Linux のブートストラップで広く利用されている PXELINUX を使います。 PXE ブートで利用可能なブートローダーは、ほかにも GRUB などがあります。
PXELINUX
PXELINUX は、軽量ブートローダーSyslinuxを PXE ブート向けにビルドした PXE アプリケーションです。 PXELINUX は Syslinux のようにブート設定をスクリプトで記述します。
PXELINUX を用いた Linux の PXE ブートのステップは以下のとおりです。
- PXE クライアントは、TFTP サーバーからロードした PXELINUX を起動する
- PXELINUX は設定ファイルを、TFTP サーバーからロードする
- PXELINUX は設定ファイルに基づいて、TFTP からカーネルを取得・起動する
PXELINUX は起動マシン毎に設定を切り替えるために、IP アドレスや MAC アドレスに対する設定ファイルを探します。
たとえば TFTP 上の "/mybootdir/pxelinux.0"
から起動した PXELINUX が、MAC アドレス "88:99:AA:BB:CC:DD"
、IP アドレス "192.168.2.91"
を持ってるとします。
その場合、PXELINUX は以下の順序で TFTP サーバー上から設定ファイルをロードを試みます。
/mybootdir/pxelinux.cfg/01-88-99-aa-bb-cc-dd
/mybootdir/pxelinux.cfg/C0A8025B
/mybootdir/pxelinux.cfg/C0A8025
/mybootdir/pxelinux.cfg/C0A802
/mybootdir/pxelinux.cfg/C0A80
/mybootdir/pxelinux.cfg/C0A8
/mybootdir/pxelinux.cfg/C0A
/mybootdir/pxelinux.cfg/C0
/mybootdir/pxelinux.cfg/C
/mybootdir/pxelinux.cfg/default
PXE ブートサーバーの実装
DHCP サーバーの改良
前回実装した DHCP サーバーを元に、TFTP の情報を加えて返すようにします。
DHCP サーバーからの応答パケットに、 ServerAddr
フィールドと BootFilename
フィールドを指定します
(GitHub 上のサンプルはハードコードではなくパラメータとして与えます)。
// dhcp.go
resp := &dhcp4.Packet{
...
ServerAddr: net.IPv4(172, 24, 0, 1), // siaddr
BootFilename: "pxelinux/pxelinux.0", // file
}
TFTP サーバーの実装
TFTP の実装には go.universe.tf/netboot/tftp
パッケージを使います。
このパッケージはブートサーバーの実装に必要な、最低限の TFTP サーバーの機能が提供されています。
TFTP サーバーの実装は以下のとおりです。
TFTPServer
が TFTP サーバーが実装された struct で、リクエストパスに対して TFTPBootDir
からの相対パスのファイルを返します。
// tftp.go
package main
import (
"io"
"log"
"net"
"os"
"path/filepath"
"sync"
"go.universe.tf/netboot/tftp"
)
type TFTPServer struct {
TFTPBootDir string
conn net.PacketConn
closed bool
m sync.Mutex
}
func (s *TFTPServer) Start(listen string) error {
srv := &tftp.Server{Handler: s.handle}
log.Printf("[INFO] Starting TFTP server on %s ...", listen)
var err error
s.conn, err = net.ListenPacket("udp4", listen)
if err != nil {
return err
}
err = srv.Serve(s.conn)
if err != nil {
s.m.Lock()
if s.closed {
err = nil
}
s.m.Unlock()
}
return err
}
func (s *TFTPServer) handle(path string, addr net.Addr) (io.ReadCloser, int64, error) {
log.Printf("[INFO] GET %s from %s", path, addr)
f, err := os.Open(filepath.Join(s.TFTPBootDir, path))
if err != nil {
log.Printf("[ERROR] %v", err)
return nil, 0, err
}
fi, err := f.Stat()
if err != nil {
log.Printf("[ERROR] %v", err)
return nil, 0, err
}
return f, fi.Size(), err
}
func (s *TFTPServer) Shutdown() error {
s.m.Lock()
s.closed = true
s.m.Unlock()
return s.conn.Close()
}
PXELINUX のセットアップ
ブートサーバーの実装ができると、TFTP サーバーが提供する Linux カーネルと initrd を配置します。 ここでは Rancher OS のネットワークブートイメージを起動します。 Rancher OS はコンテナ実行環境に最適化された Linux ディストリビューションの 1 つです。
ブートサーバーを起動する作業ディレクトリに tftpboot
というディレクトリを作成し、以下のレイアウトでファイルを配置します。
tftpboot
`-- pxelinux
|-- pxelinux.0 ... PXELINUX本体
|-- ldlinux.c32 ... Syslinux 5.0から必要になったたモジュール
|-- pxelinux.cfg
| `-- default ... デフォルトのPXELINUXの設定
`-- rancher
|-- vmlinuz ... Rancher OSのカーネル
`-- initrd ... Rancher OSの設定initrd
OS イメージを配置
vmlinuz
と initrd
は、Linux のカーネルと initrd です。
Rancher OS のイメージは GitHub レポジトリの Releases で配布されています。
PXELINUX を配置
PXELINUX は Linux ディストリビューションが提供してるパッケージ、または Syslinux のダウンロードページから入手できます。
パッケージに含まれるpxelinux.0
ファイルと ldlinux.c32
を tftpboot/pxelinux
以下に配置します。
PXELINUX の設定を配置
PXELINUX の設定ファイルはpxelinux.cfg/default
に保存します。
設定ファイルは以下の通りです。
default rancher
prompt 0
label rancher
kernel rancher/vmlinuz
initrd rancher/initrd
append console=ttyS0 rancher.autologin=ttyS0
kernel
でカーネルイメージのパスを、initrd
で initrd のパスを指定します。
また append
でカーネルオプションを追加します。
ここでは 2 つのオプションを指定します。
console
パラメータには、カーネル起動メッセージの表示やログインに用いるコンソールを指定します。
QEMU はクライアントらかの標準入出力を、仮想シリアルコンソールに接続します。
そのときのシリアルコンソールが ttyS0
で、これを指定しないと起動時に何も表示されません。
ブートサーバーの起動
TFTP のデバッグ
上記で TFTP サーバーの実装と準備が整いました。 それでは TFTP サーバーが意図通り動くか確認してみましょう。
今回追記した Go のプログラムをビルドして起動します。 67 番ポート、69 版ポートに LISTEN するには管理者権限が必要なので sudo で実行します。
go build . && sudo ./building-boot-server
TFTP のデバッグには curl コマンドが利用できます。 TFTP サーバー上のファイルが意図通り返されるか確認してみましょう。
$ curl tftp://127.0.0.1/pxelinux/pxelinux.cfg/default
default rancher
prompt 0
...
VM を起動
さて、TFTP サーバーのチェックができればいよいよ起動です。 VM の起動は第 1 回に利用したシェルスクリプトを利用します。
まずは仮想ネットワークを作成します。
sudo ./bin/setup network --name br0 --address 172.24.0.1/16
そしてクライアントの VM を起動します。
sudo ./bin/setup node --network br0
するとブートサーバーの標準出力に、DHCP リクエストや TFTP リクエストのログが流れるはずです。
クライアントマシンは、DHCP の応答から IP アドレスを設定すると、まず TFTP サーバーから PXELINUX をロードします。 そして PXELINUX は設定ファイルをロードして、その設定ファイルに基づきカーネルと initrd をロードします。
カーネルと initrd をロードできると Linux の起動を開始します。 しばらく待って Rancher OS のログイン画面が表示されれば成功です。
おわりに
遂に Linux がブートしました。 簡単なコードでしたが、ネットワークブートや Linux のブートストラップの手順がなんとなくわかったかと思います。 また今回は PXELINUX の設定ファイルを固定ファイルから渡しましたが、もちろん TFTP サーバーがプログラマブルに返すこともできます。
途中のコードは若干省略している部分もあるので、フルバージョンは以下のレポジトリを参照してください。
それでは次回もお楽しみに!