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 ネットワークプログラミング入門 を持っている。入門編だけど、この際読み直しておくかな。ウィルスに遭遇するよりは、ずっといいでしょう。
実際に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の中で、あちこちを引き回されている(その為、無駄に遅い)けど、その外側では、すんなりと行くよ。