ntpd server (3)

近頃、よく麒麟をみるぞ。ああ、麒麟と言えば普通はあのうざい広告をしてるNHKが閃くけど、今回は別のキリンね。あっ、ビールじゃ無いやつ。そんな事を言えば、キリンの首はどうして長くなったって言う、ダーウィンの種の起源でも勉強してるの?

いいえ、違います。業界用語でキリンと言えば、高所作業車の事ね。よく見かけるんだ。 この間も家の前に止まって、首を長くしてたぞ。電柱から長いケーブルを住宅に張りこんでいた。

お付きの交通誘導員に聞いてみた。このケーブルって光が通るやつですか。Yes。

以前に書いた事があるけど、オリンピックを8Kで見ましょうが、進展してんだな。

地元のプロバイダーもやっと準備が整ったみたいで、最後の追い込みに入った風。プロバイダーでマルチキャストなら許したる。

それがなんだ。本物のNHKは、放送をネットに解禁するとか。そんなにネットを圧迫すると、定額制を崩して、従量制の議論が出てきちゃうじゃないですか。NTTとかは、虎視眈々と転換を狙っているからね。

NHKプラスは、視聴者にプラスにならないで、NTTとかの土建業にプラスになるだけじゃん。全く余計な事をするもんだ。反省しろよ >放送業界

まあNHKがパンドラの箱を開ける決意をしちゃった。それはNHKがプラスになる事を願っての事。視聴者がプラスになるなんて、幻想だぞ。ネーミングからしてそうなってるわな。

become daemon

ntpdでは、デーモンを作って、下仕事をそれに任せている。

オイラーもデーモン大好き人間。今は無きBSDマガジンの表紙を飾っていたっけ。また、昔のFreeBSDでは、デーモンのキャラ絵がbootの時に出て来た。

そんなんで、我がHPにicoアイコンとして、デーモンの画像を載せている。ブラウザのタグの所はブックマークをかわゆく飾ってくれている。

一度、デーモンの作り方を復習しておくかな。

プロセスのデーモン化

OpenBSDでは、これらに加えて、権利の剥奪と言うか、使える権利を与える(事で、間接的に制限)事をやってる。

DESCRIPTION
     The pledge() system call forces the current process into a restricted-
     service operating mode.  A few subsets are available, roughly described
     as computation, memory management, read-write operations on file
     descriptors, opening of files, and networking.  In general, these modes
     were selected by studying the operation of many programs using libc and
     other such interfaces, and setting promises or execpromises.

一例を上げれば、

ntp.c:149:      if (pledge("stdio inet", NULL) == -1)
ntp_dns.c:101:  if (pledge("stdio dns", NULL) == -1)

こんな風になってる。ntpのデーモンは、stdioに関する(許されたシステムコール)とinet群が利用できるって訳。 それぞれの群と言うかグループの説明は、pledegに列挙されてる。例えば

           stdio      The following system calls are permitted.  sendto(2) is
                      only permitted if its destination socket address is
                      NULL.  As a result, all the expected functionalities of
                      libc stdio work.

                      clock_getres(2), clock_gettime(2), close(2),
                            :

こんな感じ。このシステムコールが発行された後は、制限以外のシステムコールでSIGABRTが発生すると言う厳しい制裁が待っている。本当かどうか、簡単なやつで調べてみる。chdirは、野放しにすると危険って事で、rpath群で制限をかける事が出来る。

#include <stdio.h>
#include <unistd.h>

int main(){
  chdir("/");
  printf("before pledge\n");
  pledge("stdio rpath", NULL);
  chdir("/");
  printf("after pledge\n");
  return 0;
}

pledgeの前後で、chdirを使ってみた。

ob$ ./a.out
before pledge
after pledge

許可が有ったので、最後まで走った。次は、rpathを削除して、chdirを禁止にする。

ob$ ./a.out
before pledge
Abort trap
ob$ gdb -q a.out a.out.core
    :
Core was generated by `a.out'.
Program terminated with signal SIGABRT, Aborted.
#0  chdir () at -:3

マニュアルの説明通りに、トラップで実行が中止された。コアを臨場すると、chdirで失敗させられている事が分かる。うっかりして危険なシステムコールを発行しても落ちるような仕組みが、簡単に導入出来るって素敵。少なくとも、selinuxよりは取り扱いが楽だな。現場に委ねると、色々とミスをやらかすものですよ。

その点、pledgeは、設計時に安全性が組み込まれますから、安心です。いわば、馬鹿避けな訳です。

imsg_compose

で、ダエモン君は、標準の入出力ラインが閉じられてしまい陸の孤島になっちゃうんだけど、 それでは困る。よそのプロセス(往々にしてntpdみたいな親分プロセス)と通信したくなる。

枕に出てきた光回線みたいなのを施設して、家庭内のTVと放送局側を結ぶみたいなことが 出来るようになってる。

光ケーブルだと、信号の下り用と登り用の2本がペアになってる。昔よく使ったLANケーブルも 同様に、2組の信号ラインを使う。片方のコネクタで送信用に割り当てられたラインをもう片方では受信のラインに割り当て。もう一本のラインも同様に接続。こういうのをクロスケーブルと言う。

このクロスケーブルが1本あると、そのケーブルを使って、パソコン同士をLANで直結出来る。手軽なんで、よく使ったものだ。そうそう、同僚にこういったLANケーブルのコネクタ接続を得意にしてる人がいた。よく、ケーブルの長さを指定してLANケーブルを作ってもらったものだ。懐かしいな。今じゃ、LANケーブルなんて、とんと見ない。みんなWiFiになっちゃったからね。

いかんいかん。imsg系を調査してるんだった。調べた限りではOpenBSDに固有の物っぽい。他のOSだと別の名前で、同様な機能を提供してるんだろうけど、深くは突っ込まない。

manによると、ソケットを2つ作る所から作業が始まる。2本の通信路を確保するんだな。

           struct imsgbuf  parent_ibuf, child_ibuf;
           int             imsg_fds[2];

           if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, imsg_fds) == -1)
                   err(1, "socketpair");

           switch (fork()) {
           case -1:
                   err(1, "fork");
           case 0:
                   /* child */
                   close(imsg_fds[0]);
                   imsg_init(&child_ibuf, imsg_fds[1]);
                   exit(child_main(&child_ibuf));
           }

           /* parent */
           close(imsg_fds[1]);
           imsg_init(&parent_ibuf, imsg_fds[0]);
           exit(parent_main(&parent_ibuf));

親・子用のバッファーと、それに付随するソケット(の配列)を用意。socketpairで、ソケットを作成。続いて、子を作成。

子はimsg_fds[0]は使わないのでclose。次にimsg_initで、もう一つのソケットとバッファーを結びつけてから、子用のmainを起動。親の方は、もう片方のソケットを使うようにして、親用のmainへ行く。以後は、バッファーにアクセスするだけだ。

           child_main(struct imsgbuf *ibuf) {
                   int     idata;
                   ...
                   idata = 42;
                   imsg_compose(ibuf, IMSG_A_MESSAGE,
                       0, 0, -1, &idata, sizeof idata);
                   ...
           }

子から親へのデータ送信。どんな種類のデータはは、imsg_composeの第二引数に指定する。データそのものは、例の場合idataになる。

           dispatch_imsg(struct imsgbuf *ibuf){
                        :
                   if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) {
                        :
                   for (;;) {
                           if ((n = imsg_get(ibuf, &imsg)) == -1) {
                               :
                           switch (imsg.hdr.type) {
                           case IMSG_A_MESSAGE:
                                   memcpy(&idata, imsg.data, sizeof idata);
                                   /* handle message received */

データを取り込む方は、多種のデータがやってくるので、それを判別して処理するためのdispatchルーチンにまとめてある。データが到着する度に、getで取り込み、タイプを判別して、該当するルーチンに処理を任せるようにしてる。

ここで、最初にimsg_readしてるのは、ソケットに溜まったものをibufに移しておく為だ。こうしておけば、より低レイヤーでのデータの流通度が上がる。実際のデータ処理で、getするのは、imsg_getになる。ibufがソケットに結び付いたバッファー。imsgは、高レイヤーのバッファーと言うか、構造になる。

低レイヤーのバッファって出てきたけど、実際は

     The imsg API defines functions to manipulate buffers, used internally and
     during construction of imsgs with imsg_create().  A struct ibuf is a
     single buffer and a struct msgbuf a queue of output buffers for
     transmission:

           struct ibuf {
                   TAILQ_ENTRY(ibuf)        entry;
                   unsigned char           *buf;
                   size_t                   size;
                   size_t                   max;
                   size_t                   wpos;
                   size_t                   rpos;
                   int                      fd;
           };

           struct msgbuf {
                   TAILQ_HEAD(, ibuf)       bufs;
                   uint32_t                 queued;
                   int                      fd;
           };

こんな説明がなされていた。TAILQとか出てくるけど、カーネルで多用されてる(他にもあるけど)構造だ。

Tail queue(TAILQ)

に詳しい解説があった。記して感謝します。キューのヘッダーを読むのは大変ですからね。

ntpctl

ntpd対になるntpctlを見て行く。これの作りは上で調べたimsg系と同じになってるから、勉強材料には丁度良いだろう。 ntpdとntpctlは実体が同じになっている。ntpdのmainの冒頭に、

        if (strcmp(__progname, "ntpctl") == 0) {
                ctl_main(argc, argv);
                /* NOTREACHED */
        }

こんな記述がある。起動されたプログラム名がntpctlなら、ctl_mainへ行け。もう帰って来る事は無いよって、コメントまで書いてある。

DESCRIPTION
     The ntpctl program displays information about the running ntpd(8) daemon.

manには、ntpdの情報表示って書いてあるしね。任意の時点にntpdの状態をモニター出来るって訳。じゃ情報をどうやって抜き出す?

上のimsg系ではforkしたら、すかさず通信路を確保してたけど。ntpctlでは、ntpdが起動時に用意してくれた名前付きの情報路を使う事になる。

FILES
     /var/run/ntpd.sock     Socket file for communication with ntpd(8).

ちょいとコードを追って行くと

        if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
                err(1, "ntpctl: socket");

        if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) == -1)
                err(1, "connect: %s", sockname);

        if (pledge("stdio", NULL) == -1)
                err(1, "pledge");

        imsg_init(ibuf_ctl, fd);

ソケットにsocknameって名前を付けてから、pledgeで以降、悪い事が出来ないように設定。そして、ソケットディスクリプタと共にimsgのイニシャライズ。

        switch (action) {
        case CTL_SHOW_STATUS:
                imsg_compose(ibuf_ctl, IMSG_CTL_SHOW_STATUS,
                    0, 0, -1, NULL, 0);
                break;
          :
	while (ibuf_ctl->w.queued)
                if (msgbuf_write(&ibuf_ctl->w) <= 0 && errno != EAGAIN)
                        err(1, "ibuf_ctl: msgbuf_write error");

ntpctlのコマンド引数から引き継いだアクションによって、リクエストを送信。

                        if ((n = imsg_get(ibuf_ctl, &imsg)) == -1)
                                err(1, "ibuf_ctl: imsg_get error");
                        if (n == 0)
                                break;

                        switch (action) {
                        case CTL_SHOW_STATUS:
                                show_status_msg(&imsg);
                                done = 1;
                                break;

答えが返ってきたら、そのタイプに従って、メッセージを表示する。

尚、対向するデータの送信元は、control.cの中に有る。 ntp engineのpidを調べてから、 ここぞと思う所にBPを貼った。そして、待機、別端末から ntpctl -s statusしたよ。

ob$ doas gdb -q ntpd -p 45424

(gdb) b control.c:234
Breakpoint 1 at 0xe1ab1ea03df: file control.c, line 234.
(gdb) c
Continuing.

(gdb) bt
#0  control_dispatch_msg (pfd=0xe1d341ac7f8, ctl_cnt=0x7f7ffffd2894)
    at control.c:234
#1  0x00000e1ab1e986cb in ntp_main (nconf=<optimized out>, pw=<optimized out>,
    argc=<optimized out>, argv=<optimized out>) at ntp.c:411
#2  0x00000e1ab1e95dc2 in main (argc=<optimized out>, argv=0x7f7ffffd2ad8)
    at ntpd.c:217

ntp_mainの中に、コントロール用(ntpctl)のルーチンが潜んでいるのね。

(gdb) l
229                             break;
230
231                     switch (imsg.hdr.type) {
232                     case IMSG_CTL_SHOW_STATUS:
233                             build_show_status(&c_status);
234                             imsg_compose(&c->ibuf, IMSG_CTL_SHOW_STATUS, 0, 0, -1,
235                                 &c_status, sizeof (c_status));
236                             break;
237                     case IMSG_CTL_SHOW_PEERS:
238                             cnt = 0;

送り出し側は、要求が有ったら、回答を組み立ててから、送信してるんだね。こうして、非同期で発生する要求に応答してるとな。

become ntpd server

以前、ゲストOSを立ち上げて、ホスト側にntpdサーバーを置いて、同期を取ろうとして失敗してた。サーバー側のntpd.confに下記のような追加を入れたよ。ホスト側に問い合わせが有ったら、答えてくれって言う直接的な設定。本当は、listen on で、127.0.0.1とかに答えさせて、それをpf(パケットフィルター)でルーチングするのが安全らしいけどね。

XXX.XXX.XXX.XXXは、ホスト側のIPアドレスです(生は恥ずかしいので、伏字で御免)

o32# cat /etc/ntpd.conf
 :
query from XXX.XXX.XXX.XXX
listen on *      ;; 必要無いはずなんだけど、これが無いとクライアントが同期せず

クライアント側で、同期を確認。成功してる。但し、同期するまで結構時間がかかる。サーバー側が1次になるんだけど、安定してないからかな?

vm# ntpctl -sa
1/1 peers valid, constraint offset -1s, clock synced, stratum 2

peer
   wt tl st  next  poll          offset       delay      jitter
XXX.XXX.XXX.XXX
 *  1 10  1   32s   32s        20.120ms    17.971ms    16.417ms

ちなみに、こちらはサーバー側ってかホスト側

o32# ntpctl -sa
1/1 sensors valid, constraint offset -2s, clock synced, stratum 1

sensor
   wt gd st  next  poll          offset  correction
vmt0
 *  1  1  0    4s   15s         0.119ms    12.345ms

クライアント側から、サーバー側にntpdが立ってるか確認。

vm# nc -u -z XXX.XXX.XXX.XXX ntp
Connection to XXX.XXX.XXX.XXX 123 port [udp/ntp] succeeded!

コネクションってか、接続状況確認。ゲストOSはqemu上で動いている。qemuがルーターの役割をしてくれて、ホスト側へルーティングしてる事が確認出来る。ああ、すっきりした。

vm# netstat
 :
Active Internet connections
Proto   Recv-Q Send-Q  Local Address          Foreign Address        (state)
udp          0      0  10.0.2.15.15711        XXX.XXX.XXX.XXX.ntp

RFC-2030

NTP、SNTP のフォーマット

眩暈がするようなページだな。送られて来るパケットはバイナリーコードとな。それじゃNTPパケットだけ受信して解析すればいいな。

Wireshark - Filter

wiresharkでは、キャプチャする時のフィルターする方法と、取り合えず何でも取り込んでおいて、表示時にフィルターする方法が有るとな。

WireShark でパケットの詳細情報まですべてテキストとして出力する方法

解析結果をASCIIで保存する方法も有る。でも、いつもながらの方法でやってみる。

ob$ doas tcpdump -i em0 -w LOG port ntp

解析したパケットをASCIIで保存するには、File -> Export Packet Dissections -> As Plain Textを選んで保存すれば良い。

日本標準時 に直結した時刻サーバ 公開NTP 日本の時刻原器に直結したサーバー。20回/時以内の接続を望んでいる。余り煩雑にアクセスすると出禁になる可能性有り。

時刻情報提供サービス for Publicとは こちらは、そんな細かい事は気にしなくてもいいのかな。

ホストOSにOpenBSD、ゲストOSにqemu上のOpenBSDを設定。ホスト側で、ntpdのサーバーを立てて、パケットをキャプチャしてみた。

Network Time Protocol (NTP Version 4, client)
    Flags: 0x23, Leap Indicator: no warning, Version number: NTP Version 4, Mode: client

こんな要求が出されるのね。UDP全体のパケットサイズは90バイト。中身は空だ。

Network Time Protocol (NTP Version 4, server)
    Flags: 0x24, Leap Indicator: no warning, Version number: NTP Version 4, Mode: server
    [Request In: 1]
    [Delta Time: 0.001693000 seconds]
    Peer Clock Stratum: primary reference (1)
    Peer Polling Interval: invalid (0)
    Peer Clock Precision: 0.015625 seconds
    Root Delay: 0.000000 seconds
    Root Dispersion: 0.000000 seconds
    Reference ID: Unidentified reference source 'HARD'
    Reference Timestamp: Feb 28, 2020 05:40:37.430409908 UTC
    Origin Timestamp: May 18, 2030 19:27:33.921580854 UTC
    Receive Timestamp: Feb 28, 2020 05:41:06.425327777 UTC
    Transmit Timestamp: Feb 28, 2020 05:41:06.425350188 UTC

それに対するサーバーの応答レスポンス。信号源はvmt0って事で、VMware tools由来。Refernce ID がHARDって刻印されてる。

servers ntp.nict.jpを指定すると、4台の並列運転をしてた。

peer
   wt tl st  next  poll          offset       delay      jitter
133.243.238.163 from pool ntp.nict.jp
    1 10  1   32s   32s        10.235ms    18.271ms     2.753ms
133.243.238.244 from pool ntp.nict.jp
 *  1 10  1   26s   32s         7.743ms    21.993ms    10.718ms
133.243.238.164 from pool ntp.nict.jp
    1 10  1   31s   33s        10.123ms    17.547ms     3.031ms
133.243.238.243 from pool ntp.nict.jp
    1 10  1    1s   33s         6.151ms    18.586ms     4.150ms

これNICTからの返答。

Network Time Protocol (NTP Version 4, server)
    Flags: 0x24, Leap Indicator: no warning, Version number: NTP Version 4, Mode: server
    [Request In: 9]
    [Delta Time: 0.019034000 seconds]
    Peer Clock Stratum: primary reference (1)
    Peer Polling Interval: invalid (0)
    Peer Clock Precision: 0.000001 seconds
    Root Delay: 0.000000 seconds
    Root Dispersion: 0.000000 seconds
    Reference ID: Unidentified reference source 'NICT'
    Reference Timestamp: Feb 28, 2020 06:38:57.000000000 UTC
    Origin Timestamp: Dec 26, 2077 21:18:35.219098295 UTC
    Receive Timestamp: Feb 28, 2020 06:38:57.779202999 UTC
    Transmit Timestamp: Feb 28, 2020 06:38:57.779203819 UTC

クロックの分解能が1us、原発なので、遅延もバラツキも有りませんと自信たっぷりなパケットが返ってきました。