ntpd packet (4)

おばあちゃんが農道の脇で焚火をしていた。燃やしてる物は落ち葉じゃなくて、果樹を選定した時に出た小枝。この時期、野焼きやら山焼きが燃え広がって山火事になるんで、春一番が吹いている時は、絶対にやらないで下さいなんてアナウンスが頻繁にあるけど、大丈夫?

まあ、無風だったし、もしもの為にタンクに水も用意されてた。おばあちゃんと少し話をする。消防署の方から、飛んできませんか? ここん所何十年とやってるけど、ダイジョブだー。 ダイオキシンが出るから焚火禁止なんて、そんなの関係ねぇー。

何でも、おばあちゃん、去年は足を痛めて畑へ出られなかったけど、良くなったし、今年は雪が なかったので、去年の分まで剪定したさ。そしたら、こったらな事(沢山出た)になって、燃やすの手間かかる。

小枝を束ねてる紐がプチ気になった。TVアンテナへの300オームリボン型の給電線を使ってたぞ。年季が入ってズタズタになってたけど、まだまだ使えそう。

懐かしい給電線だ。電波少年だった頃、水平ダイポールアンテナを上げたんだけど、それへの給電線をどうするかで悩んだ。アンテナのインピーダンスは73オームなので、75オームの同軸ケーブルを使えば、ほぼマッチングする。理屈では分かっているんだけど、先立つ(金)ものがない。

ままよと、町の電気屋でアルバイト。報酬は300オームのリボンケーブルで貰った。一体ミスマッチで、どんだけSWRが悪くなっていたんだやら?

不整合 による 電力損失 計算 ツール

VSWR=4 CQ誌のお歴々からは、馬鹿にされる値だな。

本当は、梯子フィーダァーを作りたかったんだけど、縄梯子みたいで、回りから白い眼で見られると言って、お袋から許可貰えなかったのさ。

マルチバンド ワイヤーアンテナまだ、頑張っておられる局がある。

ntp client by python3

相変わらずntpdと戯れている。時刻の問い合わせぐらいは、すっきり書いてみたいものだ。応答性を担保する為、UDPを使っているんで、取り扱いは簡単しょ。

何でも有るpythonだと、ntplibとかを使うのが当たり前らしいけど、それじゃ面白い所が 隠れてしまう。ってんで、好き者が書いてたやつを、継ぎはぎしてみた。48バイトの塊を投げると(冒頭は、我はクライアント也、ntp3で返答宜しくの依頼フラグ)、答えがその塊に突っ込まれて返ってくる。1900年起点なんで、Unixのepocに直す為、引き算して調整してる。なんて事が分かる。

import sys
import socket
import struct

target_host = sys.argv[1]
target_port = 123
msg = '\x1b' + 47 * '\0'

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto(msg.encode(), (target_host,target_port))
msg, addr = client.recvfrom(4096)

NTP_PACKET_FORMAT = "!12I" # network-order uint[12]
NTP_DELTA = 2208988800     # (1970-01-01 - 1900-01-01) in seconds

unpacked = struct.unpack(NTP_PACKET_FORMAT,
         msg[0:struct.calcsize(NTP_PACKET_FORMAT)])
print( addr )
print( unpacked[10] + float(unpacked[11])/2**32 - NTP_DELTA )

こちらは、ntp.nict.jpに有った、Tips。

    NTP時刻とPOSIX時刻のオフセット(2208988800秒)を補正してからctime()等を使用する。

    #include <stdio.h>
    #include <time.h>

    main() {
    unsigned long ntptime;
    time_t posixtime;

    posixtime = (time_t)(ntptime - 2208988800UL);
    printf("%s\n", ctime(&posixtime));
    }

ちょいと走らせてみる。ぐぐるさんも、サーバーを提供してるそうなので。

o32$ python3 ntp.py time.google.com
('216.239.35.0', 123)
1583036810.7336655
o32$ python3 ntp.py ntp.nict.jp
('133.243.238.243', 123)
1583036826.0516272

ntp packet

64Bit機をntpdのサーバーに、32Bit機をクライアントにして、クライアント側でgdbする。

(gdb) bt
#0  client_dispatch (p=0x6e915800, settime=0 '\000', automatic=0 '\000') at client.c:284
#1  0x18ff0673 in ntp_main (nconf=0xcf7f4358, pw=0x5e487000, argc=2, argv=0x6e9101d0) at ntp.c:402
#2  0x18fedd1a in main (argc=<optimized out>, argv=0xcf7f44a4) at ntpd.c:217

最初UDPなパケットを受信するには recvfrom を使うものと思っていた。その方向でソースを眺めていたんだけど、さっぱり見あたらない。しょうがないのでそれらしい所で網を貼ったら引っかかってきた。でつらつらと追って行ったら recvmsg に遭遇。引数の扱いが違うだけで、一族なのね。ちっとも知らんかったぞ。で、パケットを送出する方も馬鹿の一つ覚えを払拭しておこう。 sendto を調べたら sendmsg も併記されてた。これで、馬鹿の二つ覚えに昇格かな。

昔の本で、 UNIX ネットワークプログラミング入門 を持っている。入門編だけど、この際読み直しておくかな。ウィルスに遭遇するよりは、ずっといいでしょう。

プロセス間通信(3)/UDP/IPとシグナル

実際にntpのパケットを受信してみる。ntpd.confを変更して、ぐぐるさん所にも問い合わせ。

ob# ntpctl -sp
peer
   wt tl st  next  poll          offset       delay      jitter
162.159.200.1 time.cloudflare.com
    1 10  3   24s   33s        -0.997ms    18.787ms     2.400ms
216.239.35.8 time.google.com
 *  2 10  1   18s   33s        -1.621ms    57.062ms     6.990ms

受け取ったパケットが下記のように分解されてる。clock.c:374あたりからステップ実行。

(gdb) p msg
$5 = {
  status = 36 '$',
  stratum = 3 '\003',
  ppoll = 0 '\000',
  precision = -25 '\347',
  rootdelay = {
    int_parts = 0,
    fractions = 43037
  },
    :
  xmttime = {
    int_partl = 1252133602,
    fractionl = 960079282
  }
}
397             p->reply[p->shift].offset = ((T2 - T1) + (T3 - T4)) / 2;
(gdb)
398             p->reply[p->shift].delay = (T4 - T1) - (T3 - T2);
(gdb) p p->reply[p->shift].offset
$6 = 0.00052618980407714844
(gdb) p p->reply[p->shift].delay
$7 = 0.019597530364990234

後で使うんで、自前の場所に格納してる。 そして、次に問い合わせる時間を

scale_interval(time_t requested)
{
        time_t interval, r;

        interval = requested * conf->scale;
        r = arc4random_uniform(MAXIMUM(5, interval / 10));
        return (interval + r);
}

で、計算してる。味噌はconf->scaleと言う係数。通常は1なんだけど、 priv_adjtime関数の中にある

update_scale(double offset)
{
        offset += getoffset();
        if (offset < 0)
                offset = -offset;

        if (offset > QSCALE_OFF_MAX || !conf->status.synced ||
            conf->freq.num < 3)
                conf->scale = 1;
        else if (offset < QSCALE_OFF_MIN)
                conf->scale = QSCALE_OFF_MAX / QSCALE_OFF_MIN;
        else
                conf->scale = QSCALE_OFF_MAX / offset;
}

この手続きにより、条件が整えば、scale値が再計算(されて大きくなる)される。尚ここで使われているQSCALE_OFF_MINは1ms、QSCALE_OFF_MAXは50msと定義されている。結構offsetが小さくないと、ポーリング間隔は伸びない事になる。(30秒から35秒の間でうろうろするのが関の山)

ちょっと統計してみる

大分メインのロジックが分かってきたと思うので、サーバーとどんなやりとりをしてるか、統計してみる。昔々の現役時代にntpを立ち上げて、何日も統計データを取った記憶があるぞ。

時刻を貰って来るサーバーをぐぐるに限定。

#!/bin/sh
while :
do
    ntpctl -sp | grep ms >> LOG
    sleep 30
done

こんなスクリプトを1時間程走らせてみた。その結果は、

ob$ head -3 LOG
 *  1 10  1   18s   34s        -0.294ms    56.262ms     5.780ms
 *  1 10  1   19s   31s        -0.052ms    56.278ms     5.802ms
 *  1 10  1   23s   34s        -0.103ms    55.952ms     5.634ms

poll間隔は30秒ぐらい。そして、オフセット、ディレー、ジッターと言う具合にデータが並んでいる。

ob$ cat LOG | awk '{print($7,$8,$9)}' | sed -e 's/ms//g' | uniq >gntp

データだけを抽出、邪魔な単位である ms を除去。そしてデータ収集タイミングによるダブりを除去。これで、当初138個有ったデータが129個になった。収集時間は、1時間強と言った所だ。

ob$ octave -q
octave:1> load gntp
octave:2> statistics(gntp)
ans =
  ;  offset     delay        jitter
   -5.171000   54.540000    2.825000  ; minimum
   -1.571000   56.694000    4.292000  ; first quartile
   -0.625000   57.669000    5.752000  ; median
   -0.026000   58.607000    9.167000  ; third quartile
    2.247000   78.186000   65.151000  ; maximum
   -0.812690   59.157178   11.438496  ; mean
    1.509067    5.150343   15.013735  ; standard deviation
   -0.483464    2.636470    2.786727  ; skewness
    3.190465    9.511486    9.950856  ; kurtosis

統計データを計算させるには、今までだとRを使うのが定番(人によってはpandaとかか)だったけど、octaveでも十分に役に立つよ。

ob$ octave -q
octave:1> load gntp
octave:2> t = [1:1:129];
octave:3> plot(t, gntp(:,1))
octave:4> hold on
octave:5> plot(t, gntp(:,2))
octave:6> plot(t, gntp(:,3))
octave:7> print -dpng 'hoge.png'

今度はグラフを書かせる。x軸と言うか時間軸用の1づつ増える配列を用意。最初にoffsetの折れ線グラフを描画。重ね書きしたいので hold on を指定してから、delayとjitterも書いてあげる。

delayが突然増えている所が有るなあ。jitterも同様になってるぞ。平日の午後なんで、誰かがネットを使ってたのかな。最近は動画を見るなんて言う贅沢がまかり通っているからね。

octave:4> corr(gntp(:,2), gntp(:,3))
ans =  0.97261
octave:5> corr(gntp(:,2), gntp(:,1))
ans = -0.39046

所で、jitterってどう算出してるの? プチ興味がある。offsetと非常に強い正の相関が確認されましたからね。

control.c/build_show_peer

        jittercnt = 0;
        cp->jitter = 0.0;
        for (shift = 0; shift < OFFSET_ARRAY_SIZE; shift++) {
                if (p->reply[shift].delay > 0.0 && shift != best) {
                        cp->jitter += square(p->reply[shift].delay -
                            p->reply[best].delay);
                        jittercnt++;
                }
        }
        if (jittercnt > 1)
                cp->jitter /= jittercnt;
        cp->jitter = sqrt(cp->jitter);

OFFSET_ARRAY_SIZEってのは、8に設定されている。すなわち直近の8回のデータについての演算。bestは、最も小さいdelayを表している(事前に算出)。それを基準に差分の2乗を足しこんでから個数で割り、その平方根を求めている。これって、基準が平均じゃなくて最小値に取った、標準偏差に他ならない。簡単に言うと、過去8回のデータにおけるバラツキ具合だな。

offset and delay

ntpパケットから直接的に得られるデータは、offsetとdelayである事が判明した。これがどのような意味を持ってるか考えたい。分かり易いように運び屋の例に置き換えてみる。

運び屋と言うと負のイメージが付きまとう。例えばヤク(薬)の運び屋とか、金塊や宝石の運び屋とか、近頃ではコロナウィルスの自覚症状無しの運び屋とかね。でも、まっとうな世界で仕事をしてる運び屋だっているぞ。

大体、運び屋って言葉が悪いよ。メッセンジャーボーイ(ガール)って言えば、有難い存在だ。 ピザのメッセンジャーボーイとか、書類のメッセンジャーボーイ、もとえバイク便ね。これらは、せいぜい、ちまちまと近所を走るか都内を駆け回るぐらい。でも、中には国をまたいで活躍する人達も居る。

ハンドキャリーで大事な荷物を超特急で運ぶ。国際的なメッセンジャーね。オイラーも現役時代にお世話になった事が有る。頼んだ荷物は空港で手渡しね。運んでくれた人は、次の便でトンボ帰り。観光も何もあったものじゃない。せいぜい空港内で、ローカル食を堪能するぐらいが関の山。

ああ、前置きが長くなった。例をあげよう。

某関西企業が、台湾に住んでいる会長に、重要書類の提出を命じられた。国際メッセンジャーの出番。時差は1時間あるな。メッセンジャーは出発/到着時に、タイムスタンプを押してもらう契約になってる。

   関西空港       桃園空港
    6 (T1)        5            日本出発
    8             7 (T2)       台湾到着
   11            10 (T3)       台湾出発
   13 (T4)       12            日本到着

上の表は鉄ちゃんご愛用の筋表を、普通のタイムテーブルにしたものだ。関西空港は日本時間を刻み、桃園空港は台湾時間を刻んでいる。それぞれの数字はローカル時刻。T1とかは、タイムスタンプ番号だ。(後で使う)

台湾では飛行機の折り返し準備に3時間かかってるなあ。飛行時間は、行きも帰りもそれぞれ2時間ってめっぽう速いな。それより、東に向かう時は、偏西風の影響で、西行きより時間がかかるはず、、なんてのは、取り合えず却下です。

で、関西空港をntpのクライアント、桃園空港をサーバーと思ってくれ。 client.cにある、計算式を再掲

         *  The roundtrip delay d and local clock offset t are defined as
         *
         *    d = (T4 - T1) - (T3 - T2)     t = ((T2 - T1) + (T3 - T4)) / 2.
delay  = (13 - 6) - (10 - 7)       =  4
offset = ((7 - 6) + (10 - 13)) /2  = -1

往復にかかった時間(飛行機の搭乗時間)は、4時間。これがdelay。時差に相当するのがoffsetだ。

ntpdで考えるなら、サーバーとの時差がZEROになるのが究極の目標だ。但し、サーバーとクライアントとの間は、不確定要素のネットワークで繋がっている。

不確定要素を排除するには、delayが小さい方が有利。delayが大きいと言う事は、ネットワーク的にあちこちを経由してる事があるからね。そして、delay時間のばらつきが小さい方が、安定してると見做せる。このばらつきはjitter(と言う標準偏差)で、評価出来るとな。

再測定

翌朝の早朝に、再測定してみた。今度は、ntp3.jst.mfeed.ad.jp と言う国内のサーバー。

 *  1 10  2   28s   33s        -0.092ms    18.140ms     3.027ms
 *  1 10  2   29s   31s         0.485ms    18.117ms     3.020ms
 *  1 10  2 1589s 1590s         0.401ms    18.014ms     2.872ms
 *  1 10  2 1559s 1590s         0.401ms    18.014ms     2.872ms
       :
 *  1 10  2 1550s 1553s         0.248ms    17.836ms     2.699ms
 *  1 10  2 1520s 1553s         0.248ms    17.836ms     2.699ms
       :
 *  1 10  2 1497s 1509s         0.255ms    18.209ms     3.490ms
 *  1 10  2 1467s 1509s         0.255ms    18.209ms     3.490ms
       :
 *  1 10  2 1514s 1519s         0.280ms    18.653ms     3.883ms
 *  1 10  2 1484s 1519s         0.280ms    18.653ms     3.883ms

特徴的なのは、起動後1時間程して、安定期に入った事。

ob$ cat LOG | awk '{print($7,$8,$9)}' | sed -e 's/ms//g' | wc
     297     891    5701
ob$ cat LOG | awk '{print($7,$8,$9)}' | sed -e 's/ms//g' | uniq | wc
     120     360    2334

安定期になると、データの更新間隔が長くなるので、ユニーク数が大幅に減っている。 以後、この120個のデータについて調べる。

octave:3> statistics(mfeed)
ans =
  ; offset        delay          jitter
   -1.06300000   17.58200000    0.97100     ; min
    0.00050000   18.17075000    2.34125000
    0.44900000   18.48400000    2.73600000  ; median
    0.96850000   18.88675000    3.18700000
   13.11100000   45.44400000   55.95100000  ; max
    1.28938333   20.45447500    7.27662500  ; mean
    2.97577016    6.20150029   13.23090344  ; std div
    3.10647021    3.26681307    2.93892575
   11.90818308   12.58107143   10.32613333

このデータをグラフ化したものを mfeed.pngに示します。青色はオフセット、赤色はディレーです。横軸は30秒間隔、最後の数ステップは安定してるので、約10分間隔ぐらいになってます。 途中で2箇所、ディレーが大幅に増大してるけど、これ何なのさ?

ちょっと悪い事

pingで、ラウンドトリップ時間を調べる。

ob$ ping time.google.com
ping: Warning: time.google.com has multiple addresses; using 216.239.35.4
PING time.google.com (216.239.35.4): 56 data bytes
64 bytes from 216.239.35.4: icmp_seq=0 ttl=128 time=64.066 ms
64 bytes from 216.239.35.4: icmp_seq=1 ttl=128 time=56.155 ms
64 bytes from 216.239.35.4: icmp_seq=2 ttl=128 time=55.639 ms
64 bytes from 216.239.35.4: icmp_seq=3 ttl=128 time=55.828 ms
64 bytes from 216.239.35.4: icmp_seq=4 ttl=128 time=55.328 ms
64 bytes from 216.239.35.4: icmp_seq=5 ttl=128 time=58.771 ms
^C
--- time.google.com ping statistics ---
6 packets transmitted, 6 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 55.328/57.631/64.066/3.093 ms
ob$ ping ntp3.jst.mfeed.ad.jp
PING ntp3.jst.mfeed.ad.jp (210.173.160.87): 56 data bytes
64 bytes from 210.173.160.87: icmp_seq=0 ttl=128 time=19.265 ms
64 bytes from 210.173.160.87: icmp_seq=1 ttl=128 time=16.021 ms
64 bytes from 210.173.160.87: icmp_seq=2 ttl=128 time=16.456 ms
64 bytes from 210.173.160.87: icmp_seq=3 ttl=128 time=16.385 ms
64 bytes from 210.173.160.87: icmp_seq=4 ttl=128 time=16.238 ms
64 bytes from 210.173.160.87: icmp_seq=5 ttl=128 time=16.136 ms
^C
--- ntp3.jst.mfeed.ad.jp ping statistics ---
6 packets transmitted, 6 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 16.021/16.750/19.265/1.134 ms

この結果はntpctlで得られるdelay時間と一致してるな。

最強のtracerouteをOpenBSDから発したら、途中でブロックされたので、Windows10のtracertを使ってもう少し深掘り。

ぐぐるの方は、17ホップ目で先に進めず。mfeedの方は、13ホップ目で到達。んTTの中で、あちこちを引き回されている(その為、無駄に遅い)けど、その外側では、すんなりと行くよ。