Goで作るモダン・ブートサーバー Day4 - PXEサーバー実装編

この記事は「Go で作るモダン・ブートサーバー」の 4 日目の記事です。 この記事では PXE ブートができるブートサーバーの実装をします。

シリーズの記事は以下のリンクからどうぞ。

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 サーバーが応答メッセージの siaddrfile フィールドにブート情報を返します。 siaddrfile フィールドは、それぞれ TFTP サーバーのアドレスとパスです (その他の DHCP メッセージ上のフィールドについては RFC 2131、または前回の記事を参照)。

PXE クライアントは上記のフィールド情報をもとに、TFTP サーバーから実行ファイルをロードします。 PXE ブートではまず最初にブートローダーを起動するのが慣例です。 今回は Linux のブートストラップで広く利用されている PXELINUX を使います。 PXE ブートで利用可能なブートローダーは、ほかにも GRUB などがあります。

PXELINUX

PXELINUX は、軽量ブートローダーSyslinuxを PXE ブート向けにビルドした PXE アプリケーションです。 PXELINUX は Syslinux のようにブート設定をスクリプトで記述します。

PXELINUX を用いた Linux の PXE ブートのステップは以下のとおりです。

  1. PXE クライアントは、TFTP サーバーからロードした PXELINUX を起動する
  2. PXELINUX は設定ファイルを、TFTP サーバーからロードする
  3. 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 イメージを配置

vmlinuzinitrd は、Linux のカーネルと initrd です。 Rancher OS のイメージは GitHub レポジトリの Releases で配布されています。

PXELINUX を配置

PXELINUX は Linux ディストリビューションが提供してるパッケージ、または Syslinux のダウンロードページから入手できます。 パッケージに含まれるpxelinux.0 ファイルと ldlinux.c32tftpboot/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 をロードします。

Linuxがブートする画面

カーネルと initrd をロードできると Linux の起動を開始します。 しばらく待って Rancher OS のログイン画面が表示されれば成功です。

Rancher OSが起動した画面

おわりに

遂に Linux がブートしました。 簡単なコードでしたが、ネットワークブートや Linux のブートストラップの手順がなんとなくわかったかと思います。 また今回は PXELINUX の設定ファイルを固定ファイルから渡しましたが、もちろん TFTP サーバーがプログラマブルに返すこともできます。

途中のコードは若干省略している部分もあるので、フルバージョンは以下のレポジトリを参照してください。

それでは次回もお楽しみに!


Profile picture

Shin'ya Ueoka

B2B向けSaaSを提供する会社の、元Webエンジニア。今はエンジニアリング組織のマネジメントをしている。