Azureの青空にDHCPサーバーを浮かべる。ついでにRDNSS

# はじめに

サーバーが老朽化してしまいました。ただの老朽化ではありません。なんせPentiumIIIで動いていたんです。入れ替えるの面倒なんで放置気味になっていき、最終的には動いているからいいやってなりました。その間にハードディスクが死んだり、CPUファンが死んだり、電源ファンが死んだりしましたけど、入れ替えるより面倒じゃないのでハードの方をチョコチョコ対応してしのいでいました。

でももう無理なようです。ついにマザーボードがおかしくなったようでbootに支障が出るようになりました。動作中は良いのですが、一度電源を切ると再起動できるかは運だめしです。

老朽化していくサーバーに対してデスクトップなどは適当に入れ替えていたため、性能の逆転現象が起きています。デスクトップの方が速いんです。ディスク容量もあるんです。

性能の良いハードを入れてVM化することも考えましたが、やっぱり逆転現象は起こるでしょう。そもそも手元にハードを置くこと自体が問題なのでしょう。VMにするなら、クラウドでもいんじゃないかという思考に落ち着くことになりました(使い方はほぼレンタルサーバーですが)。

日経コンピュータに『個人情報などをクラウドに置くことが問題になるような法的な根拠はない』とかいう理由でクラウド化を進める企業が紹介されていました。普通は流出とか技術面を心配するもんだから、法的な根拠があろうが無かろうが関係ないんじゃないかと思ったのですが、天下の日経コンピュータが言っているので間違いないですね。私、権威主義者ですから。

クラウドに置いてはいけないという法的根拠はないんです。だからクラウド化、進めましょう

クラウドにAzureを選んだのは深い理由はありません。手元に使えるクレジットがあったからです。ビデオ会議と一緒で最初の頃に苦労された方がいて、今はその結果が反映済の状態なので、どこも似たりよったりではないでしょうか。他人の苦労をかすめ取るのです。苦労された方、ありがとうございます。

Azureに初めてログインして、いろいろ試していると翌日には日本マイクロソフト株式会社の営業の方から電話がありました。そんなこと今まで無かったので凄くびっくりしました。何かお困りのことがありましたら、私宛にご連絡ください的な感じです。

その時に営業さんがAzureをアジュールと呼んでいました。Azureがまだメジャーではなかった頃。Azureという見慣れない単語を見て、なんて読むんだろうと思い、ググりました。その頃は今みたいに英単語サイトも無かったので、blogが何かでアジューやアジャ、アジャーみたいな感じになるというのを見つけ、それ以来、アジャーと呼んでいました。
だからMicrosoftな環境でミスすると『アジャーやっちゃった』です。でもこれが使えないことを知りました。アジャー。

# DHCPサーバーのクラウド化
クラウド化するサーバー機能にはいろいろあるのですが、このドキュメントではDHCPサーバーを扱います。以下のような環境を作ることを目指します。

![目標とするDHCPサーバー環境](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555574/e0a163ec-2a9d-d340-3c84-8f2eb481f255.png "目標とするDHCPサーバー環境")

自組織とクラウドとの通信用に固定IPが最低一個は必要です。以降はRFC6890の例示用のIPアドレスから拝借して、203.0.113.1を固定IPとして使います。またAuzreのグローバルIPアドレスは198.51.100.1を使います。

# Azureのサポートサーバーソフト
Azureではサーバーソフトの要件というのがあって、DHCPサーバーはサポートしないといことが、この<a href="https://support.microsoft.com/en-us/help/2721672/microsoft-server-software-support-for-microsoft-azure-virtual-machines">ドキュメント</a>で明記されていました。以下に引用します。

> The following roles are not supported on Microsoft Azure virtual machines:
>
>    Dynamic Host Configuration Protocol Server
>    Hyper-V (Hyper-V role is supported in Azure Ev3, and Dv3 series VMs only)
>
>    Rights Management Services
>    Windows Deployment Services

でも、ここでいうDHCPサーバーのサポートは、Auzre上にある仮想ネットワーク内での話しです。

AzureにVMを作成する際、特に指定をしないと10.0.0.0/24ような仮想ネットワークに接続されます。このネットワークへのIPアドレスの払い出しはAzureが管理しているDHCPサーバーによって行われます。仮想ネットワークで独自DHCPサーバーを構築すると、この仕組みと干渉するためサポート外とされています。

今回行おうとしているのは、現在動かしている自分組織のプライベートネットワークに対するDHCPサーバーの構築です。幸いなことに、このプライベートネットワークは10.0.0.0/24ではないので、Azureの仮想ネットワークと干渉することはありません。

# サーバーOSの環境
以降、具体的なコマンドなども登場しますので使用するサーバー環境についてまとめておきます。

<dl>
<dt>OS</dt>
<dd>Debian 10で確認。Debian 11,12も可</dd>
</dl>

2021年9月の追記
Debian 11がリリースされたので入れ替えました。ソースを確認したところ修正元ソースには変更が入っていないようなので、このドキュメントの記述通りで問題ないです。upgradeで/etc/defaultのisc-dhcp-relayが作られてしまうので、そこだけ注意してください。

2022年10月の追記
Debian 11で<a href="https://www.debian.org/security/2022/dsa-5251">isc-dhcpのsecurity update</a>が行われました。再びソースを確認したところ修正元ソースには変更が入っていないようなので、このドキュメントの記述通りで問題ないです。upgradeで/etc/defaultのisc-dhcp-relayが作られるのも一緒です。

2023年5月の追記
Debian 11.7で<a href="https://tracker.debian.org/news/1423008/accepted-isc-dhcp-441-23deb11u2-source-into-proposed-updates/">isc-dhcpのproposed-updates</a>が行われました。再びソースを確認したところ修正元ソースには変更が入っていないようなので、このドキュメントの記述通りで問題ないです。upgradeで/etc/defaultのisc-dhcp-relayが作られるのも一緒です。

2023年7月の追記
Debian 12がリリースされました。このバージョンからisc-dhcpのベースバージョン4.4.1から4.4.3へバージョンアップされています。色々変わっていると嫌だなと思っていたのですが、このドキュメントで触っているソースはほとんど変わっていませんでした。行番号などを取り直したので、<a href="#remember-the-source-will-be-with-you-always">別な節</a>を作ってますが、内容はこれまでと同じなので、まずは一通り目を通してからDebina 12の節に行ってください。

# AzureでDHCPサーバーを動かす。
動かすには特別なことはほとんどありません。単純にapt installでDHCPサーバーをインストールするだけです。

```Bash
root@1giantstar:~# apt insall isc-dhcp-server
```
インストール後、/etc/dhcp/dhcpd.confを編集し必要な情報を記載します。IPv6のことを考えなくて良くするため、/etc/default/isc-dhcp-serverの定義にINTERFACESv4の指定しましょう。INTERFACESv4を設定し、INTERFACESv6を空("")にしておけば、起動スクリプトで-4が追加され、IPv4のみになります。

その後、仮想ネットワークと干渉しないように、iptablesで固定IPからの要求のみに対応するようにしましょう。この設定で仮想ネットワークからのDHCPクライアント要求はサーバーに通知されなくなります。

```Bash
root@giantstar:~# iptables -I INPUT -i eth0 -p udp ! -s 203.0.113.1 --dport 67 -j DROP
```

最近のlinuxカーネルはインターフェース名としてenpとかensとかが使われるのですが、Auzreはeth0じゃないとうまく動かないようです。まぁこの辺りはAzureでdebianを動かす話なので、本ドキュメントでは扱いません。

外部からAuzreに接続することになるので、ネットワークセキュリティグループの設定が必要です。固定IPからのdhcpポート(67)接続を通すように設定してください。もしロードバランサーを使っているなら、NATの設定も必要です。

# DHCPリレーオプション
DHCPリレーの構成が以下のようになっている場合、192.168.100.0/24からのDHCPクライアント要求は、DHCPリレーエージェントに中継されます。リレーエージェントの中継先はDHCPサーバーである192.168.900.1です。リレー時、DHCPクライアント要求にはリレーエージェントのIPアドレスが、Gateway IP Address(giaddr)として追加されます。以下の例ではIPアドレスはDHCP要求を受け付けているens2s0のアドレスである192.168.100.1が使用されます。

![DHCPリレーのリクエストと回答](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555574/59b9fa52-428e-cdda-29d9-348cfcb6231b.png "DHCPリレーのリクエストと回答")

DHCPサーバーでは要求パケットがリレーされている場合、自身のインタフェースアドレスではなく、giaddrを基準にIPアドレスの配布基準を決定します。例の場合は192.168.100.0/24のネットワークであると判断し、192.168.100.1へ割り当て結果を送信します。

クローズドなネットワークでは、192.168.900.1から192.168.100.1までのネットワークルートは経由するルーターに設定されているので問題ありません。しかし今回のケースにようにDHCPサーバーを外部に設置した場合、問題となります。

外部サーバーでは以下のような構成になりますが、Azure上に存在するDHCPサーバーにens2s0のアドレスである192.168.100.1を通知しても、Azureからの送信では192.168.100.1までのルーティングが存在しないため、結果として不到達となります。見え方としてリレーエージェントからDHCPサーバーへの要求は到達しますが、割り当て結果が届かないという状態なります。

![DHCPリレーがうまくいかない](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555574/39f3c66e-3d66-ba01-c6ff-2862eba515e6.png "DHCPリレーがうまくいかない")

この問題に対応するには、RFC3527で定義されているLink Selection sub-optionを使用する必要があります。

RFC3527はRFC3046で定義されたリレーオプションの追加サブオプションの定義です。RFC3527をサポートするということは、RFC3046のリレーオプションもサポートする必要があります。

RFC3046はDHCP要求パケットにリレー専用のオプションを定義しています。RFC3046で定義しているのは、Agent Circuit IDやAgent Remote IDといったサブオプションで、主にRAS(Remote Access Server)でDHCPリレーエージェントを実行する場合の追加情報です。例えば、RASに複数のモデムが存在する場合、モデム単位にCircuit IDを振って、どのCircuit IDからの要求なのかをDHCPサーバーで判断できるようにします。

RFC3527は、このリレーオプションのサブオプションとしてLink Selectionを追加します。Link Selectionはgiaddrを基準としてたIPアドレスの配布を別なIPアドレスに変更するためのサブオプションです。Link Selectionを使用することにより以下のようなIPアドレスをDHCPサーバーに渡すことが可能となり、不到達の問題が回避できます。

![DHCPリレーでのLink Selectionの動き](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555574/ae22424d-992c-7f76-8759-4965c5511cb4.png "DHCPリレーでのLink Selectionの動き")


Debianでリレーエージェントを構成してみましょう。

```Bash
root@dwarfstar:~# apt insall isc-dhcp-relay
```

でインストールします。

iscのDHCPリレーエージェントで、RFC3527を使用する場合、giaddrに設定したいインターフェースを-Uオプションに指定します。Link Selection側(DHCPクライアント要求を受ける側)は、引数未指定でインターフェース名をとして指定します。

```Bash
ens2s0 -U ens3s0
```

このオプションを/etc/default/isc-dhcp-relayに指定すれば先の例のように動作します。

# It's a trap!
これでAzureでDHCPサーバーが動くようになりました。めでたしめでたし。ではありませんね。DHCPリレーエージェントをDebianで動かしたので、自組織のネットワークとInternetの間のルーターがDebianである前提になっています。昔はこんな環境もありましたが、今はHGWや専用ルーターなどを使ってInternetに接続しますよね。私のところもそうです。以下のような機材を用意しました。

![新しいネットワーク構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555574/a43b9f8e-c016-379b-5d0a-0f253982899e.png "新しいネットワーク構成")

ルーターがYAMAHAなのは、これまでの実績からです。NVR700WはDHCPリレーエージェントをサポートしていますので、設定してしまいましょう....なんと。RFC3527サポートしてない。ダジゲデ。

一応、機器を手配する前にDHCPリレーエージェントが動くことは確認したんですけどね。キーワードとして、RFC3527が無いから嫌な予感はしてたんですよ。もっと高いルーター買わないとダメなんですかね。でもオーバースペックなんですよね。お高いし。

万策尽きたのでしょうか?。とりあえず老朽化したサーバーの中でで比較的新しいAtomサーバーにDebianをインストールしてプライベートネットワーク内に配置しました。こいつでDHCPリレーエージェントを動かせばなんとかなるんじゃないですか?

![Debianを設置してみる](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555574/249219a9-dc9f-b346-c217-a6878e94f1d2.png "Debianを設置してみる")

だめです。-Uオプションでens2s0を指定してもgiaddrには203.0.113.1が入りません。192.168.100.2が設定されます。まぁ仕組み上は当たり前ですよね。NATのアドレスを知っているのはNVR700Wだけなんですから

# Use the Source, Luke. Let go, Luke.
本当に万策尽きたのでしょうか?。-Uにens2s0を指定すると、Link Selectionにはens2s0のアドレスである192.168.100.2が設定されるようです。あとは、giaddrにNATのアドレスが入ってくれれば....ソースいじちゃいましょうか?簡単そうだし。

```Bash
root@dwarfstar:~# pwd
/root
root@dwarfstar:~# mkdir src
root@dwarfstar:~# cd src
root@dwarfstar:~/src# apt install dpkg-dev
root@dwarfstar:~/src# apt source isc-dhcp-relay
```

これでsrc配下にパッケージの元のとなるソースがダウンロードされてisc-dhcp-4.4.1ディレクトリが作成されます。このディレクトリはdebian用のbuildスクリプトが反映されたソースツリーです。

```Bash
root@dwarfstar:~/src# apt build-dep isc-dhcp-relay
```

これでコンパイルに必要な依存ライブラリがインストールされます。それでは修正に入りましょう。

```C:isc-dhcp-4.4.1/relay/dhcrelay.c
  1374            /* RFC3527: Use the inbound packet's interface address in
  1375             * the link selection suboption and set the outbound giaddr
  1376             * to the uplink address. */
  1377            if (adding_link_select) {
  1378                *sp++ = RAI_LINK_SELECT;
  1379                *sp++ = 4u;
  1380                memcpy(sp, &giaddr.s_addr, 4);
  1381                sp += 4;
  1382                packet->giaddr = uplink->addresses[0];
  1383                log_debug ("Adding link selection suboption"
  1384                       " with addr: %s", inet_ntoa(giaddr));
  1385            }
```

1382行目が-Uで指定されたインターフェースのIPアドレスをgiaddrに設定しているコードです。以下の1383,1384行のようにしてしまいましょう。

```C:isc-dhcp-4.4.1/relay/dhcrelay.c
  1375            /* RFC3527: Use the inbound packet's interface address in
  1376             * the link selection suboption and set the outbound giaddr
  1377             * to the uplink address. */
  1378            if (adding_link_select) {
  1379                *sp++ = RAI_LINK_SELECT;
  1380                *sp++ = 4u;
  1381                memcpy(sp, &giaddr.s_addr, 4);
  1382                sp += 4;
  1383                //packet->giaddr = uplink->addresses[0];
  1384                packet->giaddr.s_addr = inet_addr("203.0.113.1");
  1385                log_debug ("Adding link selection suboption"
  1386                       " with addr: %s", inet_ntoa(giaddr));
  1387            }
```

もう一つ、DHCPサーバーからの回答でgiaddrを判定している部分があります。以下です。

```C:isc-dhcp-4.4.1/relay/dhcrelay.c
   846        /* Find the interface that corresponds to the giaddr
   847           in the packet. */
   848        if (packet->giaddr.s_addr) {
   849            for (out = interfaces; out; out = out->next) {
   850                int i;
   851    
   852                for (i = 0 ; i < out->address_count ; i++ ) {
   853                    if (out->addresses[i].s_addr ==
   854                        packet->giaddr.s_addr) {
   855                        i = -1;
   856                        break;
   857                    }
   858                }
   859    
   860                if (i == -1)
   861                    break;
   862            }
   863        } else {
   864            out = NULL;
   865        }
```

853行目が判定部分です。packet側には変更した203.0.113.1が入っているので元に戻します。

```C:isc-dhcp-4.4.1/relay/dhcrelay.c
   846        /* Find the interface that corresponds to the giaddr
   847           in the packet. */
   848        if (packet->giaddr.s_addr) {
   849            for (out = interfaces; out; out = out->next) {
   850                int i;
   851    
   852                for (i = 0 ; i < out->address_count ; i++ ) {
   853                    packet->giaddr.s_addr = out->addresses[i].s_addr;
   854                    if (out->addresses[i].s_addr ==
   855                        packet->giaddr.s_addr) {
   856                        i = -1;
   857                        break;
   858                    }
   859                }
   860    
   861                if (i == -1)
   862                    break;
   863            }
   864        } else {
   865            out = NULL;
   866        }
```
 
853行目のように行を追加してください。

修正前の行番号と修正後の行番号に差があるのは、行追加を行っている関係です。修正前の方を参考にしてください。

この修正で動くはずです。コンパイルしてみましょう。

```Bash
root@dwarfstar:~/src# cd isc-dhcp-4.4.1/
root@dwarfstar:~/src/isc-dhcp-4.4.1# pwd
/root/src/isc-dhcp-4.4.1
root@dwarfstar:~/src/isc-dhcp-4.4.1# dpkg-buildpackage -rfakeroot -uc -b
```

これでdebianのbuildスクリプトによってconfigureなど必要な設定が動き、.debパッケージが作成されます。作成場所は一つ上のディレクトリです。

試しに動かしてみましょう。まず最初にDHCPサーバーからの返事がこのマシンに届くようにしないといけません。先の例のようにDebianルーターになっているように見せるということです。これにはNVR700Wの静的マスカレードを使います。以下のコマンドです。

```Bash
nat descriptor masquerade static 1 1 192.168.100.2 udp dhcps
```

NVR700Wでなくても普通のルーターなら同じような設定が可能でしょう。NATアドレスの67ポート(dhcps)をDebianサーバーに向ける設定にすれば良いだけです。

ルーターによっては、inputフィルターの設定が必要な場合があるでしょう。Azureのグローバルアドレス(この例では198.51.100.1)からの67ポートへの入力を許可してください。私はNVR700Wの動的フィルタを使用しているので不要でした。

ではいよいよ起動です。以下でDHCPリレーエージェントを起動してDHCPクライアントを適当に動かしましょう。

```Bash
root@dwarfstar:~# /root/src/isc-dhcp-4.4.1/relay/dhcrelay -U ens2s0 198.51.100.1
```

確認できたでしょうか?万策は尽きてませんでした。実はここの修正と同じようなことやっているところがあります。Cumulus Networksとい会社の自社Linuxです。マニュアルを見ると-Uオプションに10.0.0.1%loというようなアドレスを指定することが可能のようです。ソース公開されていないので詳細は不明ですが、giaddrに設定するアドレスを引数として拡張されているのではないかと思います。私の目的には上で十分なのでそこまでやりません。

# 環境セットアップ
うまく動いたので環境を整備しましょう。コンパイルしたパッケージをインストールしてしまう方法もあるのですが、更新で上書きされてしまう可能性があります。changelogをいじって、コンパイルした方が新しいと認識させる方法もありますが、そういう小細工を行うと本当に更新が必要かどうかが分からなくなってしまいます。方針としてコンパイルしたものを別サービスとして動かすことにします。

まずは、パッケージの方を動かなくしましょう。systemctlでdisableにするとinit.dなどの修正が入るようなので、これもやめて以下のようにします。起動スクリプトでこのファイルが無い場合は起動しないようになっているためです。

```Bash
root@dwarfstar:~# cd /etc/default
root@dwarfstar:/etc/default# mv isc-dhcp-relay isc-dhcp-relay.norun
```

コンパイルした分の起動スクリプトを作りましょう。元はパッケージのもので良いでしょう。

```Bash
root@dwarfstar:/etc/default# cd /etc/init.d
root@dwarfstar:/etc/init.d# cp isc-dhcp-relay isc-dhcp-relay-powered-by-chokon
```

起動スクリプトの先頭のコメントはLSBとかいう/etc/rcX.dへのシンボリックを作るためのメタ情報です。でもただのコメントなので注意しましょう。説明以外の部分は触らないようにしましょう。systemctlコマンドはこのコメントを見て正しい起動順を認識します。ここの文法は私も良く分かっていません。覚えようかなと思っているうちにsysv initの時代が終わってsystemdの時代が始まってしまったので、結局覚えないままです。

```Bash:/etc/init.d/isc-dhcp-relay-powered-by-chokon
#!/bin/sh
#
#

### BEGIN INIT INFO
# Provides:          isc-dhcp-relay-powered-by-chokon
# Required-Start:    $remote_fs $network
# Required-Stop:     $remote_fs $network
# Should-Start:      $local_fs
# Should-Stop:       $local_fs
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: DHCP relay Powered By Chokon
# Description:       Dynamic Host Configuration Protocol Relay Powered By Chokon
### END INIT INFO

# It is not safe to start if we don't have a default configuration...

# Source init functions
. /lib/lsb/init-functions

DHCRELAYPID=/var/run/dhcrelay.pid
OPTIONS="-4  -U ens2s0 198.51.100.1"
EXECBIN="/root/src/isc-dhcp-4.4.1/relay/dhcrelay"

case "$1" in
	start)
		start-stop-daemon --start --quiet --pidfile $DHCRELAYPID \
				  --exec $EXECBIN -- -q $OPTIONS
		;;
	stop)
		start-stop-daemon --stop --quiet --pidfile $DHCRELAYPID
		;;
	status)
		test -e $DHCRELAYPID
		;;
	restart | force-reload)
		$0 stop
		sleep 2
		$0 start
		;;
	*)
		echo "Usage: /etc/init.d/isc-dhcp-relay-powered-by-chokon {start|stop|restart|force-reload}"
		exit 1 
esac

exit 0
```

元のスクリプトには、/etc/deafult配下のチェック等ありましたが、不要なのでばっさりカットです。またbinaryの場所も起動にマウントされているのでコンパイルソースのものをそのまま使っています。必要なら修正してください。

```Bash
root@dwarfstar:/etc/init.d# systemctl is-enabled isc-dhcp-relay-powerd-by-chokon
isc-dhcp-relay-powered-by-chokon.service is not a native service, redirecting to systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install is-enabled isc-dhcp-relay-powered-by-chokon
disabled
root@dwarfstar:/etc/init.d# systemctl enable isc-dhcp-relay-powered-by-chokon
isc-dhcp-relay-powered-by-chokon.service is not a native service, redirecting to systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install is-enabled isc-dhcp-relay-powered-by-chokon
enabled
```

これでサービスが有効になりました。startコマンドで起動できることを確認してください。

```Bash
root@dwarfstar:/etc/init.d# systemctl start isc-dhcp-relay-powered-by-chokon
root@dwarfstar:/etc/init.d# systemctl status isc-dhcp-relay-powered-by-chokon
● isc-dhcp-relay-powered-by-chokon.service - LSB: DHCP relay Powered by chokon
   Loaded: loaded (/etc/init.d/isc-dhcp-relay-powered-by-chokon; generated)
   Active: active (running) ...
     Docs: man:systemd-sysv-generator(8)
    Tasks: 1 (limit: 1059)
   Memory: 1.6M
   CGroup: /system.slice/isc-dhcp-relay-powered-by-chokon.service
           └─1023 /root/src/isc-dhcp-4.4.1/relay/dhcrelay -q -4 -U ens2s0 192.51.100
....
```

# May the Source be with you.
ソースをいじるのはやりすぎでしょうか?仕様上問題のある修正ではありませんよね。実装が無いだけです。もしソースをいじらないのであれば、自分でコードを書いてしまうのが一番の解決方法だと思いますが、DHCPプロトコルを完全に網羅するコードを自力で書くのはなかなか大変だと思います。まぁ人様のソースですが、修正後のものはinhouseとして扱えば、自分で書いたものと運用上の差はあまり無いように思えます。

あとはセキュリティ更新ですね。元のソースにセキュリティfixが入るかもしれません。この対応は、セキュリティ問題が発生した段階で、新しいソースを入手して同じ手順を踏むしかありません。ただ今回修正したコード付近で問題が発生する可能性はそんなに大きくないように思えます。そのため修正箇所を再調査するといった手間はそんなに多くはないでしょう。そもそも自力でコードを書いていたら、人目に触れる機会が低くなるので、誰もセキュリティ問題を指摘してくれないです。そう考えると再コンパイルの手間も安定運用の駄賃と思った方が良さそうです。

どのように考えるにしても、ソースに手を入れるということは、管理も必要となるということです。その覚悟は必要でしょう。

# Cloudへの道は半ば
当初の計画では組織内のサーバーを全部排除することでした。今回の対応はこの目的から逸脱してしまいました。

NVR700WにもDHCPサーバー機能は存在するので、そもそもAzureなんかで動かさなければ良いという考えもあるのですが、これまで使っていた設定ファイルをそのまま使いたいです。NVR700WのDHCPサーバー機能とisc-dhcpの一番の違いは、MACアドレスでIPアドレスを固定に払い出す場合の設定方法です。

isc-dhcpは、固定払い出しの設定とレンジによる払い出しの設定が独立しているので、レンジは192.168.100.100-192.168.100.200にして、固定で192.168.100.3を割り当てるという設定が可能です。しかしNVR700WのDHCPサーバーは固定で割り当てる場合もレンジ内である必要があります。スコープという機能を使えば同様のことはできるのですが、かなり無理矢理感のある設定方法です。そんなことする方が悪いんじゃんという意見もあると思いますが、今までやっていた設定が使えないのを目の当たりにすると、モヤモヤするのです。

今は余りのAtomサーバーでDebianを動かしていますが、そんなに負荷のかかる処理でも無いので、そのうち、RaspberryPiとかOpenWRTとかに移行してしまおうと思います。そうすれば専用機器みたいなものなのでサーバー排除計画が達成したことになるでしょう。

# おまけ。どうせならRDNSS。
実はもう一つ困ったことがありました。クラウド化に伴ってネットワーク環境の見直しも行いました。IPv6 Readyです。このIPv6ですが、DNSのIPv6アドレス配布に問題があります。

<a href="https://www.janog.gr.jp/meeting/bof-lt-night-2/doc/bof-lt-night-2_lt_RDNSS_hideki-sato.pdf">ここ</a>に詳しい説明がありますが、Androidだけ鬼子になっています。

IPv6導入前から、この点は把握していたので、自組織へのIPv6アドレス配布はDHCPv6は使用せず、ルーター広告(RA)を使う方針としていました。DNSサーバーアドレスの配布はRDNSSで行うつもりです。でもNVR700WってRDNSSの広告できないんです。また調査不足か。

2021年9月の追記
NVR700Wのファームに修正が入り、RDNSSの広告ができるようになったようです。でもDHCPリレーの方は変わっていないで、私は試していません。もしRDNSS絡みでこのドキュメントを参照されているなら本体のマニュアルを確認された方が良いです。

今回、サーバーを残すことにしたので、こいつにRDNSSを広告するようにしてもらいましょう。以下でインストールです。

```Bash
root@dwarfstar:~# apt install radvd 
```

radvdは通常はルーターで動作し、IPv6アドレスの広告が主な機能です。RDNSSの広告は追加機能的な位置づけです。今回はルーターではなく、普通のサーバーで動かすので少し設定にコツが必要です。以下のように設定ファイルを作りましょう。IPv6アドレスは例示用のアドレスです。

```Bash:/etc/radvd.conf
interface ens2s0 {
# 定期的にルータ広告を送る
    AdvSendAdvert on;
# デフォルトルーターでない
    AdvDefaultLifetime 0;
    AdvCurHopLimit 0;
    AdvDefaultPreference low;
    AdvSourceLLAddress off;
    RDNSS 2001:db8::1 {
    };
};
```

ルーター広告にはデフォルトルーターを通知するフィールドが存在します。このフィールドにはルーター広告を発行しているインターフェースのIPアドレスが設定されます。今回のケースではens2s0のIPv6アドレスです。しかしこのサーバーはルーターではありません。このためクライアントはルーターでないサーバーにパケットを送信して送信障害が発生してしまいます。

IPv4の場合、こういう場合は正しいデフォルトルーターを通知する仕組みがあったと思うのですが、IPv6はそういうのは無いようです。

このような状態にならないようにするには、デフォルトルーターの生存期間を0に設定する必要があります。先の設定例ではAdvDefaultLifetimeがこれに相当します。生存期間が0の場合、クライアントはデフォルトルーターとして採用しなくなります。RDNSSのようなオプションだけを広告したい場合は、この設定を行う必要があります。

あとはAndroidでの確認ですが、AndroidにDNSサーバーを設定を確認する標準GUIは無いようです。getpropという開発ツールを使えば確認できるようですが、私は、Termuxを使いました。このアプリにはgetpropを発行するパッケージがあるので別途インストールしDNSサーバーが上記の設定通りになることを確認できます。これでIPv6 Readyです。<a href="http://www.kame.net/">KAME</a>を踊らせましょう。


# この中に何人か仲間外れがいる
出来上がった環境で運用を始めたのですが、しばらくするとAzureのDHCPサーバーのログに不思議なログが出始めました。

```Bash
Nov 29 20:43:40 giantstar dhcpd[53950]: DHCPREQUEST for 192.168.100.24 from 00:00:5e:00:53:00 via eth0
Nov 29 20:43:40 giantstar dhcpd[53950]: DHCPACK on 192.168.100.24 to 00:00:5e:00:53:00 via eth0
```

このログが結構頻繁に出ています。おかしいなと思って調べると他のアドレスでも同様のログが出ています。一度でだすとリトライするようでかなり頻繁に出ます。

最初に見つけたログは、リコーの複合機のものなのですが、この子は中でNetBSDが動いています。なのでsyslogが参照できるので確認してみました。すると以下のようにDHCPサーバーへの接続エラーが大量に出力されていました。

```Bash
#[ncsd(77)]20/11/30 06:50:13 DHCP server not found  ERR: 
#[ncsd(77)]20/11/30 06:52:45 DHCP server not found  ERR: 
#[ncsd(77)]20/11/30 06:55:18 DHCP server not found  ERR: 
#[ncsd(77)]20/11/30 06:57:48 DHCP server not found  ERR: 
...
```

でも、そもそもこのログを見るためにログインしてますのでIPアドレス自体はちゃんと取得できているようです。何が起きているのでしょうか。電源を切って再投入してログを観察しました。以下は電源投入時のdiscoverです。


```Bash
Nov 30 15:39:52 giantstar dhcpd[53950]: DHCPDISCOVER from 00:00:5e:00:53:00 via 203.0.113.1
Nov 30 15:39:52 giantstar dhcpd[53950]: DHCPOFFER on 192.168.100.24 to 00:00:5e:00:53:00 via 203.0.113.1
Nov 30 15:39:52 giantstar dhcpd[53950]: DHCPREQUEST for 192.168.100.24 (192.51.100.1) from 00:00:5e:00:53:00 via 203.0.113.1
Nov 30 15:39:52 giantstar dhcpd[53950]: DHCPACK on 192.168.100.24 to 00:00:5e:00:53:00 via 203.0.113.1
```

リレーサーバー経由で払い出されています。このログと先のログを良く見比べると、viaの後が違います。リレー経由のログは固定IP(203.0.113.1)が表示されていますが、先のログではインターフェース名(eth0)となっています。

この部分がインターフェース名になるのは、リレー経由で接続していない場合です。どうもこの子達は直接DHCPサーバーへ接続しているようです。その結果、<a href="#dhcpリレーオプション">DHCPリレーオプション</a>で説明した外部サーバーの構成と同様に、戻りアドレスがプライベートアドレスとなり、DHCPACKが受信できずリトライを繰り返しているようです。

他のクライアントはこのような動作にはならないので、なぜこのような動作になるのかは不明なのですが、このような動作をするクライアントはすべて組み込み系のDHCPクライアントです。HPのLaserjetとかPanasonicの電話機とかです。ちょっと気になる点として、この子達は、割り当て時のClient identifierを認識しないようでした。実装が古いというか限定的なようなのでリレーサーバーの認識も行われないのかもしれません。

なぜクライアントがAzure上のDHCPサーバーに気づくのでしょうか?最初のdiscover時はリレーサーバー経由になっているようです。その謎を探るためパケットを観察しました。その結果、最初のDHCPOFFER時にServer identifierオプションとしてDHCPサーバーのアドレス(192.51.100.1)が渡ってました。

Auzre側のDHCPサーバーの設定を調べると、/etc/dhcpd.confに以下のような記述がありました。これが原因のようです。

```Bash
server-identifier giantstar;
```

ここをリレーサーバーのIPアドレスに買い替えると、エラーログはでなくなりました。

```Bash
server-identifier 192.168.100.2;
```

iscのDHCPサーバーでは複数のサブネットへのIPアドレス配布をサポートしています。定義の際は、サブネット単位に&#123;&#125;で区切ってアドレスレンジなどを記述します。未確認なのですがserver-identifierは、サブネットの&#123;&#125;単位に定義可能なのだと思います。

# Remember, the Source will be with you, always.
Debian 12では、isc-dhcpのベースバージョンが4.4.1から4.4.3へバージョンアップされました。でもこのドキュメントで変更しているソース(dhcrelay.c)は、ほとんど一緒です。行番号に多少変更があるので、変更前の行番号とソースを以下に書いておきます。

giaddrを設定しているコードは以下の行番号になりました。1249行と1250行をドキュメント通りに変更すれば良いでしょう。
```C:isc-dhcp-4.4.3/relay/dhcrelay.c
  1241			/* RFC3527: Use the inbound packet's interface address in
  1242			 * the link selection suboption and set the outbound giaddr
  1243			 * to the uplink address. */
  1244			if (adding_link_select) {
  1245				*sp++ = RAI_LINK_SELECT;
  1246				*sp++ = 4u;
  1247				memcpy(sp, &giaddr.s_addr, 4);
  1248				sp += 4;
  1249				packet->giaddr = uplink->addresses[0];
  1250				log_debug ("Adding link selection suboption"
  1251					   " with addr: %s", inet_ntoa(giaddr));
  1252			}
```

DHCPサーバーからの回答部分は以下です。

```C:isc-dhcp-4.4.3/relay/dhcrelay.c
   783		/* Find the interface that corresponds to the giaddr
   784		   in the packet. */
   785		if (packet->giaddr.s_addr) {
   786			for (out = interfaces; out; out = out->next) {
   787				int i;
       
   788				for (i = 0 ; i < out->address_count ; i++ ) {
   789					if (out->addresses[i].s_addr ==
   790					    packet->giaddr.s_addr) {
   791						i = -1;
   792						break;
   793					}
   794				}
       
   795				if (i == -1)
   796					break;
   797			}
   798		} else {
   799			out = NULL;
   800		}
```

789行目を同様に変更すれば良いです。

upgradeで/etc/defaultのisc-dhcp-relayが作られてしまうのも同じです。mvしてオリジナルのisc-dhcp-relayが起動しないようにしてください。

# あとがき

昔、新世紀エヴァンゲリオンの考察同人誌というのを読んだことがあって、”あとがき”みたいな部分に、”この本を書く上でいろいろなことを調べた(死海文書?とか)。異論や反論があるなら、最低限自分と同じレベルの調査を行ってからじゃないと聞く耳持たない。”というようなことが書いてあって同人誌っていうのは凄い世界だなぁと思いました。