NTPサーバ名をDHCPで取得して設定する方法

# 動機

孤立したローカルネットワーク内にある複数のホスト(Linux)の時刻合わせのため、そのうちの1台でNTPサーバを動かし、他をNTPクライアントで時刻同期をとることとした。各クライアントホストでは、DHCPで配信されたNTPサーバアドレスを設定することにした。

# クライアントの設定の方が難儀だった。

NTPサーバの設定、およびDHCPサーバでNTPサーバ情報を配信する設定に関しては、色々な所に書かれている。一方でクライアントの設定に関しては環境依存が大きく適切な情報を見つけるのに時間を要した。クライアント側(Linux)が起動時に行なっている処理は、概ね下記のような手順をとっているようだ。

  1. DHCPクライアントが終了時にスクリプト(hook)を起動する。
  2. DHCPクライアントが起動したスクリプトがNTPクライアントの設定ファイルを更新する。
  3. 更新された設定ファイルを読んで、NTPクライアントが起動される。

ここで環境依存が発生するのは、**DHCPクライアント(`dhclient`(ISC-DHCP), `dhcpcd`, `NetworkManager`のinternal dhcp client, ...)と、NTPクライアント(`ntpd`, `openntpd`, `chrony`, `systemd-timesyncd`, ...)の組み合わせ**が多いことによる。Linuxディストリビューションによってはバージョンアップの際にデフォルトとして採用するDHCP,NTPパッケージを変わることがあるようで、それに気づかないとハマる。デフォルトでない(=使用されない)DHCPクライアントやNTPクライアントのパッケージもインストールされていたりすると、**使用されることのない設定ファイルを書き換えて所期した動作をせず悩む**ということがおきる。

# Raspberry Pi OS の場合
Raspberry Pi OS (64bit, Debian buster based)では、デフォルトのDHCPクライアント=`dhcpcd`, NTPクライアント=`systemd-timesyncd`となっている。DHCPで呼ばれるhookは、`/lib/dhcpcd/dhcpcd-hooks/50-ntp.conf`のようである。このファイルには、`ntpd`,`openntpd`,`chrony`用の記述はあるが`systemd-timesyncd`用の記述がない。[公式サイトのフォーラムの情報](https://www.raspberrypi.org/forums/viewtopic.php?t=217832#p1340336)を元に、下記を追記したところ、システム起動時にNTPサーバが動的に設定されて時刻合わせがされるようになった。**`/etc/dhcp/dhclient-exit-hooks.d/timesyncd`というファイルが存在してそれらしい実装が含まれているが、こちらは使われていないので注意が必要である。**

```patch:/tmp/dhcpcd_ntp_conf(修正部分;diff出力)
--- /etc/dhcpcd.conf.orig	2019-11-13 23:44:50.000000000 +0900
+++ /etc/dhcpcd.conf	2020-09-16 08:28:53.595999273 +0900
@@ -30,7 +30,7 @@ option classless_static_routes
 option interface_mtu
 
 # Most distributions have NTP support.
-#option ntp_servers
+option ntp_servers
 
 # A ServerID is required by RFC2131.
 require dhcp_server_identifier
```


```sh:/lib/dhcpcd/dhcpcd-hooks/50-ntp.conf(追記)
# Set NTP servers for systemd-timesyncd

confd=/run/systemd/timesyncd.conf.d

if [ -n "$new_ntp_servers" ] ; then
    set_servers() {
        mkdir -p "$confd"
        ( echo "# Created by dhcpcd hook";echo "[Time]"; echo "NTP=$new_ntp_servers" ) > "$confd/dhcp-ntp.conf"
        # Tell timesyncd it has an updated configuration
        systemctl try-reload-or-restart systemd-timesyncd
    }
    if $if_up; then
        set_servers
    fi
fi
```

ちなみに、`systemd-timesyncd`のデフォルトの設定ファイル(`/lib/systemd/system/systemd-timesyncd.service.d/disable-with-time-daemon.conf`)では、他のNTPクライアントやVirtualBoxのサービスの実行ファイルが存在していたら何もしないようになっているので、`50-ntp.conf`は置き換えでなく追記にしておいた方が無難とおもわれる。

# Ubuntu MATE 20.04 LTS の場合

`Raspberry Pi`用のイメージ(`ubuntu-mate-20.04.1-beta2-desktop-arm64+raspi.img`)の場合、使われているのは`NetworkManager`のinternal dhcp clientと、`systemd-timesyncd`の組み合わせのようである。デフォルトで`dhclient`もインストールされており、`/etc/dhcp/dhclient-exit-hooks.d/timesyncd`も用意されているので、コマンドラインで手動で`dhclient`を実行すると時刻の同期がされる。そこで、`/etc/NetworkManager/NetworkManager.conf`の`[main]`セクションに`dhcp=dhclient`と追記してinternal dhcpの代わりに`dhclient`が使われるようにすれば良いかと思いきや、ログを見ると`dhclient`がエラーを吐いており、また思ったタイミングでhookが呼ばれない。

しかたがないので`dhclient`を使うのは諦め、`/etc/dhcp/dhclient-exit-hooks.d/timesyncd`を参考に`NetworkManager`のDispatherスクリプトを用意することにした。[公式マニュアル](https://developer.gnome.org/NetworkManager/stable/NetworkManager.html)にしたがって環境変数や判定文字列を修正して、`/etc/NetworkManager/dispatcher.d/90-dhcp-timesyncd`を作成する。

```sh:/etc/NetworkManager/dispatcher.d/90-dhcp-timesyncd
#!/bin/sh

TIMESYNCD_CONF=/run/systemd/timesyncd.conf.d/01-dhclient.conf

timesyncd_servers_setup_remove() {
    if [ -e $TIMESYNCD_CONF ]; then
        rm -f $TIMESYNCD_CONF
        systemctl try-restart systemd-timesyncd.service || true
    fi
}

timesyncd_servers_setup_add() {
    if [ ! -d /run/systemd/system ]; then
        return
    fi

    old_ntp_servers=$(sed -ne 's/^NTP=//gp')
    if [ -e $TIMESYNCD_CONF ] && [ "x$DHCP4_NTP_SERVERS" = "x$old_ntp_servers" ]; then
        return
    fi

    if [ -z "${DHCP4_NTP_SERVERS}" ]; then
        timesyncd_servers_setup_remove
        return
    fi

    mkdir -p $(dirname ${TIMESYNCD_CONF})
    cat <<EOF > ${TIMESYNCD_CONF}.new
# NTP server entries received from DHCP server
[Time]
NTP=$DHCP4_NTP_SERVERS
EOF
    mv ${TIMESYNCD_CONF}.new ${TIMESYNCD_CONF}
    systemctl try-restart systemd-timesyncd.service || true
}

logger -i -t "$0"  "action=${NM_DISPATCHER_ACTION}:NTP=${DHCP4_NTP_SERVERS}"

case $NM_DISPATCHER_ACTION in
    up|dhcp4-change)
        timesyncd_servers_setup_add
        ;;
    down)
        timesyncd_servers_setup_remove
        ;;
    *)  :
        ;;
esac
```

このファイルは、オーナーをrootにして実行パーミッションを与えておく必要がある。

```
% ls -l /etc/NetworkManager/dispatcher.d/90-dhcp-timesyncd
-rwxr-xr-x 1 root root 1140 Apr  2 02:55 /etc/NetworkManager/dispatcher.d/90-dhcp-timesyncd
```

このスクリプトを置くことで、時刻の同期がされるようになった。

# JetPack 4.4 (Ubuntu 18.04 LTS based)の場合

Ubuntuの一つ前のバージョンでも使われているのは`NetworkManager`と、`systemd-timesyncd`の組み合わせのようである。そのため、前節(Ubuntu MATE 20.04 LTS)と同じスクリプトで動くと思いきや、そのままでは期待した通りに動作してくれなかった。`networkmanager-dispatcher`のバージョンの違いのせいか、環境変数`$NM_DISPATCHER_ACTION`がセットされないようである。そのため、かわりに実行時の第2引数を評価するように下記のように修正することで期待した動作がされるようになった。

```diff:NetworkManager-dispatcherのバージョン違いに対応
--- Ubuntu_MATE-20.04LTS/etc/NetworkManager/dispatcher.d/90-dhcp-timesyncd	2020-09-20 21:52:17.950941231 +0900
+++ JetPack-4.4/etc/NetworkManager/dispatcher.d/90-dhcp-timesyncd	2020-09-22 01:18:46.160691469 +0900
@@ -34,9 +34,9 @@ EOF
     systemctl try-restart systemd-timesyncd.service || true
 }
 
-logger -i -t "$0"  "action=${NM_DISPATCHER_ACTION}:NTP=${DHCP4_NTP_SERVERS}"
+logger -i -t "$0"  "action=${NM_DISPATCHER_ACTION:-${2}}:NTP=${DHCP4_NTP_SERVERS} $@"
 
-case $NM_DISPATCHER_ACTION in
+case ${NM_DISPATCHER_ACTION:-$2} in
     up|dhcp4-change)
         timesyncd_servers_setup_add
         ;;
```
余談だが、Ubuntu 18.04 LTSの`NetworkManager`は、internal dhcp clientではなく、`dhclient`を利用しているようだ。

# Ubuntu Server 20.04 LTSの場合

`systemd-networkd`のinternal dhcp clinetと`systemd-timesyncd`の組み合わせとなっている模様。とくに何もしなくてもDHCPのNTPサーバが設定されるようになっている。`systemd-networkd-dispatcher`に関してはあまり情報を見つけられなかったので、どのような仕組みでDHCPで取得されたアドレスが`systemd-timesyncd`に渡されているのかは未解明。

# CentOS 7.8 (20.03), CentOS 8.2 (20.04)の場合

`NetworkManager`と`chrony`の組み合わせになっていて、何もしなくてもDHCPのNTPサーバが設定されるようになっている。余談だが、CentOS 7では、`dhclient`が使われていて、CentOS 8では`NetworkManager`のinternal dhcp clientが使われているようだある