dullwhaleのメモ帳

何度も同じことを調べなくてよいように...

色んなOSでNTP時刻合わせ

加筆中 調べたら追記する

LAN内にそこそこのノードがいると、時刻合わせについて気を配る必要が出てくる。 特に、そのノードがサーバとして機能している場合にはログの突き合わせなどで、粗くとも秒オーダーの同期精度は欲しくなる。 ここではLAN内に構築されているNTP(network time protocol)サーバに同期する前提で考える。

また、LAN内に固定配置され動かされない想定の設定と、DHCP(dynamic host configuration protocol)で動的にLAN内に入る想定の設定を記載しておく。

Linux

timesyncdを使うシステム

systemd-timesyncd、もしくは単にtimesyncdはSNTP(simple network time protocol)クライアントである。 最近の新しいLinuxディストロならデフォルトでインストールされ、起動設定がなされていることも多い。 それまでの定番であったntpdなどと違い、SNTPのクライアントとしての機能しかないシンプル・軽量なデーモンである。 SNTPはNTPのサブセットであるから、NTPサーバ側でSNTPに対応するために特別な設定などは原則必要ない。

timesyncdの設定はtimesyncd.confに記述する。 Raspberry Pi向けのDebian 11では/etc/systemd/timesyncd.confに配置されていた。 最低限、以下の2行さえあれば動く。

timesyncd.conf

[Time]
NTP=${ここにNTPサーバ}

書き換え後は以下のようにデーモンの再起動と確認を行う。

# デーモン再起動。reloadには対応していないようだった。
sudo systemctl restart systemd-timesyncd
# 稼働状況を確認する。
systemctl status systemd-timesyncd

デーモンから以下のようなメッセージが出力されていれば同期に成功している。

systemd-timesyncd[${PID}]: Initial synchronization to time server ${同期先のホスト}.

Windows 11

固定の端末

「設定」アプリを開き、時刻と言語 > 日付と時刻 > その他の時計 とクリックする。

「日付と時刻」ウィンドウが開くから「インターネット時刻」のタブを選択して、「変更の設定(C)...」ボタンをクリックする。

「インターネット時刻設定」ウィンドウに同期先NTPサーバを入力して「OK」ボタンを押す。 入力するNTPサーバの指定はIPアドレスでもドメイン名でも良い。

DHCP端末向け

端末側の設定は行わず、サーバ側で対処する。

デフォルトのNTPサーバのドメインを偽装する

LAN内のDNSキャッシュサーバで、WindowsのデフォルトのNTPサーバのドメイン名であるtime.windows.comをLAN内のNTPサーバに解決させる。

macOS

DHCP端末向け

端末側の設定は行わず、サーバ側で対処する。

デフォルトのNTPサーバのドメインを偽装する

LAN内のDNSキャッシュサーバで、macOSのデフォルトのNTPサーバのドメイン名であるtime.apple.comをLAN内のNTPサーバに解決させる。

DHCPオプションを使ったOSに依存しない設定方法

DHCPで付加的な情報をクライアントに通知する「オプション」機能を用いて、NTPサーバのIPアドレスを通知できる。 ここではIPv4で用いられる従来のDHCP(v4)に絞って言及する。

RFC(request for comments) 2132の8.3節の記載から、オプションのコード42でNTPサーバのIPv4アドレスを通知できる。 DHCPサーバにこれを設定しておけばよい。 ただし、この情報を受け取ったDHCPクライアントが情報通りのNTPサーバを優先するかどうかは処理系依存だから、上記のような他の手段による設定も合わせて実施すべきだろう。

OpenWrt 23.05.5をルータとするネットワークではGUAとULAの同時運用をしない方が良い

OpenWrt 23.05.5をルータとして構築しているネットワークにおいてはグローバルユニークアドレス(GUA: global unique address)とユニークローカルアドレス (ULA: unique local address)の同時運用を避けた方が良い。 将来、OpenWrtのバージョンが上がった際に挙動が修正されているか確認するためのメモを残しておく。

設定項目やドキュメントを見落としている可能性もあるが、どうやらバージョン23.05.5ではルータ配下のネットワークのノード(ホスト)がGUAとULA両方を保持することを想定して実装されていないようだ。 細かい設定は省くが、以下のように二者択一のようだった。

  • ノードがGUA(IPv4におけるグローバルIPアドレスのようなもの)を得てインターネットと通信できる状態にすると、ULAで互いに通信できなくなる。
  • ULAで互いに通信できるようにすると、NATなしでインターネットと通信ができなくなる。

OpenWrtのWikiにも以下のような記述がある。

Bugs have been reported in how OpenWrt handles both GUAs and ULAs assigned on a network in some circumstances, so it is preferable to disable ULA assignment unless it is necessary.

[OpenWrt Wiki] IPv6 troubleshooting

共通の前提条件や背景

  • IPv6への移行を進めるため、積極的にIPv6を利用したい。
  • IPv6IPv4のデュアルスタックで構築している。
  • ネットワーク内にサーバが存在するため、変化しない固定のIPv6アドレスとしてULAを設定したい。
  • 複数のVLANが存在し、ULAでは対応するサブネットIDを割り当てている。
  • インターネットサービスプロバイダ(ISP: internet service provider)からGUA用に/64の1つのプレフィックスしかもらえない。
  • IPv6では積極的にはネットワークアドレス変換(NAT: network address translation)、Linux用語ではIPマスカレードを行わないという思想に従い、NATは使わない。

前者の状況の説明

ルータ広告(RA: router advertisement)やDHCPv6(dynamic host configuration protocol for IPv6)でルータ管理下のホスト全てがGUAを取得できる。 全てのノードがインターネットと通信できる。

LAN側のinterfaceにULAを設定している。 ノードにもULAを設定している。

tcpdumpで確認すると以下の挙動が分かる。

  • OpenWrt側に付加したULAでICMPv6(internet control message protocol for IPv6)の近隣広告(NA: neighbor advertisement)もRAも送信されない。
    • 明示的にルータ要請(RS: router solicitation)、近隣要請(NS: neighbor solicitation)を送信しても反応がない。
  • OpenWrtが配下のノードのULAをneighborとして認識していない。pingなどを行おうとするとDestination Unreachableになる。

GUAならば全てRFC通りに動作しているように見える。

後者の状況の説明

RAに対し、ULAのprefixを応答してしまう。 結果として、ルータ配下のノードがGUAを設定できない。 ただし、ULAに対するNS、NAは機能し、ルータ側からホストのULAへのpingが通る。

NATの設定をしていないから、IPv6でインターネットと通信できない。

サブネットIDが違うULAとの通信が完全に機能するか否かは未確認

現状の妥協案

GUAでインターネットと通信できることを優先する。 LAN内のノード間の通信はIPv4のプライベートアドレスで行う。

OpenWrtでBusyBox ntpdを用いてNTPサーバ機能を提供する

この記事ではOpenWrtにプリインストールされているBusyBox ntpdを使ってLAN内のノードにNTPサーバ機能を提供する方法について説明する。 続く文章では、初めにBusyBox ntpdが有用な状況と、その機能について説明する。 続いて、OpenWrtにおけるBusyBox ntpdの具体的な設定方法を説明し、合わせて設定すると便利なDHCPオプションについても述べる。 最後に、参考情報としてNTPサーバが機能しているかテストする方法と、RTCを備えていないマシンでNTPサーバを稼働させることに対する懸念について書き残している。

目的と背景

小規模なネットワークであっても、複数サーバのログを照合しようとすると時刻同期が重要になる。 このため、LAN(local area network)内にNTP(network time protocol)サーバが欲しくなる。 ただし、自分の環境での時刻同期の要求精度は100 msオーダーである。 これより細かい精度で同期していても恩恵が薄い。

この記事ではOpenWrt 23.05.5を用いて簡単に、程々の性能のNTPサーバ機能を提供することを目的とする。

BusyBox ntpd

ルータOSであるOpenWrtにはBusyBoxがプリインストールされている。 このBusyBoxにはNTPクライアント/サーバ機能も備わっている。 ソースコードのコメント文には、以下の記述があるからOpenBSDで開発されているNTPサーバOpenNTPDを元に作られているらしい。

NTP client/server, based on OpenNTPD

精度の高い時刻同期や、高負荷が想定される場合にはより高性能/高機能なNTPサーバを使用すべきだが、小規模なネットワークではこれで要件を満たせることがある。

OpenWrtにおけるBusyBox ntpdの設定

OpenWrtにおけるNTPサーバの設定は次に示すファイルで行う。 このファイルはOpenWrt標準の設定ファイルの1つであり、設定に当たって複雑な手順は必要ない。

⚠️ 注意

これはあくまでOpenWrtの仕組みを利用したものであり、他の汎用的なLinuxディストリビューションにおいては異なる方法で設定が必要なことに注意せよ。

cf. https://openwrt.org/docs/guide-user/base-system/system_configuration

/etc/config/system

config system
        # NTPの設定に無関係の設定項目は省略
        option zonename 'Asia/Tokyo'
        option timezone 'JST-9'

config timeserver 'ntp'
        option enable_server '1'
        list server 'ntp.nict.jp'
        list server 'ntp1.v6.mfeed.ad.jp'
        list server 'ntp2.v6.mfeed.ad.jp'
        list server 'ntp3.v6.mfeed.ad.jp'

上記は日本標準時JST: japan standard time)に同期することを目的に、時刻に関係する項目だけを記載している。 それぞれの設定値と意図について簡単に説明する。

次の示すブロックはシステム全般に関する設定である。 タイムゾーンJSTとするために2つの設定を行っている。

config system

次に示すブロックはNTPに関する設定を行う。

config timeserver 'ntp'

次のように設定することでBusyBox ntpdをクライアントだけでなくNTPサーバとしても稼働させる。 このオプションのデフォルト値は0であり、サーバ機能の無効化を意味する。

option enable_server '1'

上位Stratumの指定は次の構文で行う。

list server 'host'

ここでは2つの観点から 国立研究開発法人情報通信研究機構NICT: national Institute of information and communications technology)とインターネットマルチフィード株式会社が提供する公開NTPサーバを指定している。

  • 正確なJSTを提供することを主目的としている。厳密に見ると+09:00すれば良いだけでなかったりする。
  • インターネット的な距離が近い。つまりレイテンシやジッタが少ない。

上位Stratumの障害やサービス停止に備えて、少なくとも2つ以上のサーバをすべき(SHOULD)である。 ここでは更なる耐障害性を目的として、上記の通り2つの組織が運営するものを指定している。 一方の組織が運営するNTPサーバ全体がサービスダウンする状況に備えている。

ホストの指定ではIPv6を利用を前提としている。 NICTのホストntp.nict.jpからはAAAAレコードとAレコードを引ける。 インターネットマルチフィードのホスト名はIPv6IPv4で異なる。 ここではIPv6用のホスト名をすべてを設定している。

optinal DHCPのオプションでNTPサーバを通知する

DHCP(dynamic host configuration protocol)では、domain name systemサーバの通知などと同じくNTPサーバについても通知できる。 この仕様はRFC(request for comments) 2132の8.3節に記載されている。 オプションのコードは42である。

なお、同RFCの3.6節にTime Server Optionという記述があるが、これはRFC 868で定義されているTime Protocolのサーバを指定するオプションであり、NTPとは異なる。 混同に注意せよ。

🛈 参考

Time ProtocolはRFC番号からも分かる通り、NTP以前に使われていた時刻を提供するための非常に簡素なプロトコルである。 現在はほぼNTPや、より高精度なprecision time protocolに取って代わられているから、ほとんどの場面で無視してよい。

OpenWrtでDHCPによりNTPサーバのアドレスを通知する設定について説明する。 DHCPの設定は次のファイルに記述する。 複数サブネットやVLANを設定している場合は、以下に示すDHCPプールが複数存在する可能性があり、必要に応じてそれぞれに設定する。

/etc/config/dhcp

# そのほかの設定項目は省略
config dhcp 'POOLNAME'
        # そのほかの設定項目は省略
        list dhcp_option '42,${OpenWrtのIPv4アドレス}'

DHCPオプションは次の構文で指定する。 42を指定しているのは先の説明の通りである。 ここでは、OpenWrtが稼働しているホスト自体をNTPサーバとして機能するよう設定したから、自身のアドレスを記述する。

list dhcp_option '${DHCPオプションのコード},${値}'

DHCPIPv4を前提に作られたプロトコルであるから、同オプションでIPv6アドレスを通知できないことに注意せよ。 RFC 2132の8.3節記述からも4オクテットのIPv4アドレスしか想定していないことが分かる。

The minimum length for this option is 4 octets, and the length MUST always be a multiple of 4.

NTPサーバをIPv6アドレスで通知したい場合、DHCPv6を使う必要がある。 その方法についてはRFC 3646を参照せよ。

なお、OpenWrt 23.05.5ではDHCPv6ではオプションを指定する方法を提供していないようだ。 ドキュメントに書かれていないだけかもしれない。

NTPサーバの動作確認

LANのノードに対しNTPサーバとして振る舞い、時刻を提供できているか簡単に確認する方法について述べる。 ここではntpdateコマンドを使う。

次に示すように、qオプションを付けて問い合わせのみを行う。 qオプションを付けない場合、ホストの時刻を同期しようとするから注意が必要である。

$ ntpdate -qv ${NTP_SERVER}
 8 Dec 22:16:49 ntpdate[5928]: ntpdate 4.2.8p15@1.3728-o Wed Feb 16 17:13:02 UTC 2022 (1)
server ${NTP_SERVER}, stratum 2, offset -0.039960, delay 0.02945
 8 Dec 22:16:49 ntpdate[5928]: adjust time server ${NTP_SERVER} offset -0.039960 sec

NTPサーバとRTCの注意

OpenWrtのNTPのページにも注意があるように、家庭用ルーターとして設計された多くのネットワーク機器はRTC(real time clock)モジュールを持たない。 Raspberry Pi 5のような新しいモデルを除き、Raspberry Piのような多くのシングルボードコンピュータも同様にRTCを持たない。 OpenWrtのwikiのNTPのページにも次の注意書きがある。

Note: most devices supported by OpenWrt do not have a hardware clock.

RTCを備えないハードウェアでNTPサーバ機能を提供するべきではない(SHOULD NOT)。 RTCを備えていない場合、起動直後は時刻が0つまりunix epochになる。 これは上位Stratumとの時刻ずれがとてつもなく大きい状態の一つと見なせる。 この状況において、NTPサーバアプリケーションの実装や設定によっては以下のように振る舞う可能性がある。

  • 上位Stratumとの同期の試みを停止する。
  • 上位Stratumとの同期完了までに長い時間を要する。
  • 上位Stratumと同期が完了していないのに、下位のノードに時刻を提供してしまう。

結果として、NTPサーバとしての稼働率を低下を危険性がある。 BusyBox ntpdの実装を詳しく調査できていないから杞憂かもしれないが、いたずらに稼働率を低下させるような構成は防ぎたい。

OpenWrt設定・運用メモ

この文章はOpenWrtの設定や運用についてハマりやすい内容のメモである。

interfaceという語が非直感的

Ciscoを含む一般的なネットワーク機器におけるinterfaceなどと用語の使い方が違う。 一般的なinterfaceは例えばマネージドスイッチにおける次のようなものを想定している。

GigabitEthernet 0/0/0

紛らわしいことに、OpenWrtではinterfacedeviceなる概念が存在する。 ざっくり言って、deviceとはIPより下の物理層を設定する概念である。 interfaceはIPレイヤ以上を設定する概念である。

だから、IPアドレスの設定を設定するときはinterfaceを弄る。 MTUはEthernetの設定であるから、deviceを弄る。

物理EthernetポートにIPアドレスを振ることなくネットワークループバックアドレスを割り振る構成の例

一般的なL2スイッチ(SW: switch)のように、各物理ポートにはIPアドレスを付与せず、ネットワークの文脈におけるループバックアドレスを付与する構成の設定例の断片を示す。

この構成では、まずtypebridgedeviceを作る。 ここで作成したdeviceはL2SWであり、基本的にEthernetフレームしか理解できない。 このL2SWに属する物理ポートを指定する。 ここではlan1lan2という2つの物理ポートを備えていると仮定する。 ここまでをconfigに表すと次のようになる。

config device
  option name 'br-lan'
  option type 'bridge'
  list ports 'lan1'
  list ports 'lan2'

この設定だけなら、挙動はlan1lan2をもつアンマネージSWと同等である。

ここに加えて、interfaceの設定を行うとマネージドL2SWのような挙動になる。 次の設定は、「物理ポートとは全く無関係」に先ほど作成したL2SWへIPの設定を行う。

config interface 'lan'
  option device 'br-lan'
  option proto 'static'
  option ipaddr '192.168.1.254'
  option netmask '255.255.255.0'

これはOpenWrtの内部へのIPの口をIPv4 192.168.1.254/24で作成する設定であり、VLANは使っていないがCiscoで言うところのSVI(switched virtual interface)が近い。 要はOpenWrtの内部に仮想的なL2SWとルートプロセッサへの口を作っていると考えると理解しやすい。

これまでの設定を合わせて 「基本的にはアンマネージドL2SWとしてふるまうが、L2SW自体にループバックアドレス192.168.1.254/24を用いてアクセスできる」挙動を実現できる。

config device
  option name 'br-lan'
  option type 'bridge'
  list ports 'lan1'
  list ports 'lan2'

config interface 'lan'
  option device 'br-lan'
  option proto 'static'
  option ipaddr '192.168.1.254'
  option netmask '255.255.255.0'

ただし、firewallの設定でinput 'ACCEPT'となっていることを確認する。 管理画面やSSH接続のためには待ち受けているローカルプロセスに到達できなければいけないから、inputを許す必要がある。

GNU拡張のFORTRANでシグナルを扱う

FORTRANでキーボード割り込みを受け取ってプログラムを安全に終了したくなった。

GNU拡張のFORTRANPOSIXシグナルを受け取ってプログラムを終了するプログラムを実装する。

前提

# FORTRANコンパイラであるgfortranをインストール
$ sudo apt install gfortran
# インストールされたgfortranのバージョンを確認
$ gfortran -v
Using built-in specs.
COLLECT_GCC=gfortran
COLLECT_LTO_WRAPPER=/usr/lib/gcc/aarch64-linux-gnu/10/lto-wrapper
Target: aarch64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Debian 10.2.1-6' --with-bugurl=file:///usr/share/doc/gcc-10/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-10 --program-prefix=aarch64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-libquadmath --disable-libquadmath-support --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --enable-fix-cortex-a53-843419 --disable-werror --enable-checking=release --build=aarch64-linux-gnu --host=aarch64-linux-gnu --target=aarch64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-mutex
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 10.2.1 20210110 (Debian 10.2.1-6)

ソースコードコンパイル

gfortranのマニュアルに記載されているサンプルコードを少しだけ改変して実装した。 ここで使っているsignal()sleep()GNU拡張であり純粋なFORTRANでは利用できないことに注意せよ。

test_signal.f90

program test_signal
  implicit none
  logical :: is_running = .true.
  intrinsic :: signal, sleep
  call signal (2, signal_handler)  ! キーボド割り込み SIGINT
  call signal (15, signal_handler)  ! killシグナル -9を付けていないときのkillコマンド SIGTERM

  do while(is_running)
    print *, "do"
    call sleep (1)
  end do
  print *, "end"

contains
  ! POSIX.1-2017:  void (*func)(int)
  subroutine signal_handler() bind(C)
    print *, 'signal_handler invoked'
    is_running = .false.
  end subroutine

end program test_signal

コンパイル

$ gfortran test_signal.f90
$ ls a.out
a.out

動作テスト

# キーボード割り込み SIGINTのテスト
$ ./a.out
 do
 do
^C signal_handler invoked
 end

# killコマンドでSIGTERMを送るテスト
$ ./a.out > output.txt &
[1] 3800740
$ kill 3800740
$ cat output.txt
 do
 do
 signal_handler invoked
 end

cf. https://gcc.gnu.org/onlinedocs/gfortran/SIGNAL.html

シェルからPythonの単一ファイル中の関数を呼ぶ方法

🙅‍♂️

python -c "import lambda_function; lambda_handler(None, None)"

🙆‍♂️

python -c "import lambda_function; lambda_function.lambda_handler(None, None)"

検索して上位に出てくる方法では上手くいかない。

前提と詳細

ファイル名 lambda_function.pyの中に次のような関数が定義されているとする。 これはシンプルなAWS Lambda関数の実装を想定している。

def lambda_handler(event, context):
    return {
        'isBase64Encoded': False,
        'statusCode': 200,
        'headers': {},
        'body': '{"result":"ok"}',
    }

この関数をシェルから実行するためには次のようにする。

python -c "import lambda_function; lambda_function.lambda_handler(None, None)"

検索して上位に出てくる次の呼び出し方ではエラーになる。

# 注意!ダメな方法
$python -c "import lambda_function; lambda_handler(None, None)"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'lambda_handler' is not defined

AWS Lambda関数とAPI Gatewayを繋ぐ際apigateway put-integrationで指定するパラメータのメモ

多くのユースケースでは次を指定せよ。

--type AWS_PROXY
--integration-http-method POST

すこしだけ説明

CLIのドキュメントを読むと、--typeとして指定する値にはいくつもの種類があるように見える。 だが、Lambda関数とAPI Gatewayを繋ぐというケースでは基本的に次の2パターンの値だけ気にすれば良い。

AWS_PROXY

「Lambdaプロキシ統合」とも説明されるタイプ。 Lambda関数とAPI Gatewayを繋ぐ際の一番シンプルで、採用されることが多い方法。 統合リクエスト/レスポンスというものを設定する必要がないから楽。

このタイプを指定した場合、--integration-http-methodはPOSTにする必要がある(MUST)。

AWS

「Lambdaカスタム統合」とも説明されるタイプ。 統合リクエスト/レスポンスというものをテンプレートドキュメントで設定し、データのマッピングの設定が必要。

Lambda側はJSONでのデータやり取りを想定しているが、外部のAPIXMLでしかやりとりできないなど、リクエスト/レスポンスの変換処理が必要な場面で使うのだろう。