RFC868 Time Protocol - インターネット時刻同期の歴史(その1)
RFC868 Time Protocol
最近の若者は /etc/inetd.conf
にある time サービス
47 #time stream tcp nowait nobody internal
48 #time stream tcp6 nowait nobody internal
...
57 #time dgram udp wait nobody internal
58 #time dgram udp6 wait nobody internal
の存在なんて知らんかもしれん、なんせ inetd(8) すらもはや触ることないしな。
これは RFC868 Time Protocol のサービス側の実装。
プロトコルはクソほど単純で
- サービスはポート 37 で待つ
- クライアントはポート 37 に接続
- サービスは時刻を 1900-01-01 00:00:00 GMT からの経過秒数として 32bit binary で返す
- エンディアン? エイプリルフールジョーク にでも従っておいて
マジでこんだけ。
実際試してみよっか、/etc/inetd.conf
で time の tcp/tcp6 を有効にして再読込する。
$ sudo perl -pi -e 's/^#(time\s+stream\s+tcp)/\1/' /etc/inetd.conf
$ sudo /etc/rc.d/inetd reload
パラノイアは /etc/hosts.allow
も設定して time 如きに適切なアクセス制御も与えてどうぞ。
もしかすると時刻から個人が特定かもしれない(ガンギマリ)。
クライアントは rdate(8) なんだけど、パケットキャプチャするまでもないプロトコルだし 通信内容みたけりゃ telnet(1) で time ポート叩けば十分である。
$ cat > cmd.txt
set tracefile time.txt
toggle termdata
open localhost time
^D
$ telnet < cmd.txt
telnet> tracefile set to "time.txt".
telnet> Will print hexadecimal representation of terminal traffic.
telnet> Trying ::1...
Connected to localhost.
Escape character is '^]'.
\351m\256\246Connection closed by foreign host.
$ cat time.txt
> 0x0 e96daea6
32bit の数値 0xe96daea6 がバイナリで送られてきたのがご理解いただけただろうか。
今回は x86_64 だから ntohl(3) 要らんし printf(1) で 10進変換し date(1) で秒数を時刻にしてみる。
$ date -r `printf "%d" 0xe96daea6`
Sat Feb 6 16:24:22 JST 2094
Epoch が 1900 vs 1970 のせいで70年ズレてしまったが現在時刻なのがお判りいただけたと思う。
ちなみに人間が読める形式で返す RFC867 Daytime Protocol を実装した daytime サービスもあるが時刻同期目的ではなくテスト用。
45 #daytime stream tcp nowait nobody internal
46 #daytime stream tcp6 nowait nobody internal
...
55 #daytime dgram udp wait nobody internal
56 #daytime dgram udp6 wait nobody internal
$ sudo perl -pi -e 's/^#(daytime\s+stream\s+tcp)/\1/' /etc/inetd.conf
$ sudo /etc/rc.d/inetd reload
$ telnet localhost daytime
Trying ::1...
Connected to localhost.
Escape character is '^]'.
Wed Feb 7 16:31:20 2024
Connection closed by foreign host.
サービス側の実装を読む
TCP/IP が実装された 1983 年リリースの 4.1cBSD の /etc/service
にすでに
12 time 37/tcp timserver
の文字があるけれども、実際に実装され実用になるのは事実 4.3BSD に inetd(8) が実装されてからである実は。
20 time stream tcp nowait root internal
127 /* Return 32 bit time since 1970 */
128 "time", SOCK_STREAM, 0, 0, machtime_stream,
...
840 /*
841 * Return a machine readable date and time, in the form of the
842 * number of seconds since midnight, Jan 1, 1900. Since gettimeofday
843 * returns the number of seconds since midnight, Jan 1, 1970,
844 * we must add 2208988800 seconds to this figure to make up for
845 * some seventy years Bell Labs was asleep.
846 */
847
848 long
849 machtime()
850 {
851 struct timeval tv;
852
853 if (gettimeofday(&tv, (struct timezone *)0) < 0) {
854 fprintf(stderr, "Unable to get time of day\n");
855 return (0L);
856 }
857 return (htonl((long)tv.tv_sec + 2208988800));
858 }
859
860 /* ARGSUSED */
861 machtime_stream(s, sep)
862 int s;
863 struct servtab *sep;
864 {
865 long result;
866
867 result = machtime();
868 (void) write(s, (char *) &result, sizeof(result));
869 }
いきなりコメント間違ってて 1970 オリジンとなっとるけど実装はちゃんと
70 年× 365 日× 24 時間× 60 分× 60 秒 プラスうるう年分 2208988800
秒補正してるから安心してほしい。
最新のコードも実装から 40 年経つのにプロトコルがダチョウ脳過ぎてコードもほぼ原形が残っている。 上記のコメントの間違いも訂正されてねえ!
307 /* Return 32 bit time since 1970 */
308 { "time", SOCK_STREAM, false, false, machtime_stream },
何の気なしに小路を曲がって路地裏入ったら終戦直後に建った粗末な小屋がいまだに現存していたみたいな感覚である。
クライアント側の実装を読む
rdate(8)
前述のとおり rdate(8) を使うわけだけど、そもそも 4.3BSD は Time Protocol より優れた時刻同期プロトコル(これはまた次回説明する)を同時に載せてきたので要らない子だったのだ。 なので N の創始者 christos@n.o が 1994 年に実装するまで存在しなかったのよね。
出自を調べるとどうやら溺れた巨人こと DEC の Ultrix が独自に実装したもののようだ。 BSD でも SysV でも平家でも源氏でもない雅な公家のご出身どすなぁ(薩摩琵琶をかき鳴らす音)。
11 /*-----------------------------------------------------------------------
12 * Modification History
13 *
14 * 4/5/85 -- jrs
15 * Created to allow machines to set time from network.
16 * Based on a concept by Marshall Rose of UC Irvine
17 * and the internet specifications for time server.
18 *
19 *-----------------------------------------------------------------------
20 */
...
225 resvalue = ntohl(*(unsigned long *)resbuf) - 2208988800l;
...
287 nowt.tv_sec = resvalue;
288 if (settimeofday(&nowt, &nowz) != 0) {
289 fprintf(stderr, "%s: Time set failed\n", argv[0]);
290 exit(1);
291 }
この実装では TCP でなくこのプロトコルの仕様上処理が煩雑になる UDP をわざわざ使ってる。
さらに
- 複数回にわたって時刻の取得を試みて
- 成功した結果からその中央値で時刻を設定する
という安全策をとってるので、コードがやってることのわりに長い。
なによりここはネットワークプログラミング講座じゃないので通信周りはバッサリ割愛し時刻合わせ部分だけ引用した。
つまり
settimeofday(2)
というシステムコールで現在時刻 struct timeval time
を更新している。
settimeofday(2)
Ultrix のカーネルソース(マスタード味)は公開されてないので、ベースとなった 4.2BSD のコードを元にここからは追っていく。
16 struct timeval time;
41 settimeofday()
42 {
43 register struct a {
44 struct timeval *tv;
45 struct timezone *tzp;
46 } *uap = (struct a *)u.u_ap;
47 struct timeval atv;
48 struct timezone atz;
49
50 u.u_error = copyin((caddr_t)uap->tv, (caddr_t)&atv,
51 sizeof (struct timeval));
52 if (u.u_error)
53 return;
54 setthetime(&atv);
55 if (uap->tzp && suser()) {
56 u.u_error = copyin((caddr_t)uap->tzp, (caddr_t)&atz,
57 sizeof (atz));
58 if (u.u_error)
59 return;
60 }
61 }
62
63 setthetime(tv)
64 struct timeval *tv;
65 {
66 int s;
67
68 if (!suser())
69 return;
70 /* WHAT DO WE DO ABOUT PENDING REAL-TIME TIMEOUTS??? */
71 boottime.tv_sec += tv->tv_sec - time.tv_sec;
72 s = spl7(); time = *tv; splx(s);
73 resettodr();
74 }
そして settimeofday(2) は現在時刻 struct timeval time を更新するだけでなく resettodr(9) を呼んでいる。
resettodr(9)
こいつは RTC(Real-Time Clock) 上にある今日の日付を現在時刻 struct timeval time で更新するやくめ。 ハードウェア叩くので MD(Machine Dependent) つまり機種依存な実装となっている。
14 #define SECDAY ((unsigned)(24*60*60)) /* seconds per day */
15 #define SECYR ((unsigned)(365*SECDAY)) /* per common year */
16 /*
17 * TODRZERO is the what the TODR should contain when the ``year'' begins.
18 * The TODR should always contain a number between 0 and SECYR+SECDAY.
19 */
20 #define TODRZERO ((unsigned)(1<<28))
21
22 #define YRREF 1970
23 #define LEAPYEAR(year) ((year)%4==0) /* good till time becomes negative */
95 /*
96 * Reset the TODR based on the time value; used when the TODR
97 * has a preposterous value and also when the time is reset
98 * by the stime system call. Also called when the TODR goes past
99 * TODRZERO + 100*(SECYEAR+2*SECDAY) (e.g. on Jan 2 just after midnight)
100 * to wrap the TODR around.
101 */
102 resettodr()
103 {
104 int year = YRREF;
105 u_int secyr;
106 u_int yrtime = time.tv_sec;
107
108 /*
109 * Whittle the time down to an offset in the current year,
110 * by subtracting off whole years as long as possible.
111 */
112 for (;;) {
113 secyr = SECYR;
114 if (LEAPYEAR(year))
115 secyr += SECDAY;
116 if (yrtime < secyr)
117 break;
118 yrtime -= secyr;
119 year++;
120 }
121 mtpr(TODR, TODRZERO + yrtime*100);
122 }
この mtpr() は VAX の MTPR 命令のラッパーとなるビルドイン関数。 CPUのさまざまなレジスタにアクセスできる、レジスタの値は以下のヘッダに定義されてる。
9 /*
10 * VAX processor register numbers
11 */
...
33 #define TODR 0x1b /* time of year (day) */
なお TODR とは Time Of Day ROM の略みたい、決してヒキガエルではない。
settimeofday(2) はすなわち Step モードである
ここまで読めば settimeofday(2) は時刻が過去に戻ることを躊躇しないことをご理解いただけたと思う。 つまりは 過去回 で説明した Step モードの正体である。
つまり time サービスと rdate(8) による時刻同期はリスクがともなうわけだ。 何度も同じ定時バッチ処理が流れ無残にも本番データは破壊されバックアップは…
わァ…
…あ…
泣いちゃった!!!
もうひとつの rdate(8)
前述の N の christos@n.o が書いた実装は彼の人柄(無関係な外野の勝手な想像)どおりに単純明快。 複数回取得しての中央値とっての安全策とかの発想はない。
125 if (read(s, &tim, sizeof(time_t)) != sizeof(time_t))
126 err(1, "Could not read data");
127
128 (void) close(s);
129 tim = ntohl(tim) - DIFFERENCE;
130
131 if (!pr) {
132 struct timeval tv;
133 tv.tv_sec = tim;
134 tv.tv_usec = 0;
135 if (settimeofday(&tv, NULL) == -1)
136 err(1, "Could not set time of day");
137 }
こちらも当初実装は settimeofday(2) を使ってる、現在は別のシステムコールが使われているのだが先の話のネタバレになるのでここまで。
miscd(Miscellaneous Daemon) とは何か
余談ではあるが Ultrix では time サービスは inetd(8) のビルドインではなく miscd として機能分割している。
15 /*-----------------------------------------------------------------------
16 * Modification History
17 *
18 * 4/5/85 -- jrs
19 * Created to serve under inetd to implement some of
20 * the cheap internet services. Based on a concept by
21 * Marshall Rose of UC Irvine and Chris Kent of Purdue.
22 *
23 *-----------------------------------------------------------------------
24 */
...
335 /* return longword giving biased time in seconds since 1900 */
336
337 } else if (strcmp(servnam, "time") == 0) {
338 if (tcpsock == 0) {
...
366 } else {
...
368 (void) gettimeofday(&nowtime, &nowzone);
369 nowbias = htonl(nowtime.tv_sec + 2208988800l);
...
375 (void) alarm(TIMEOUT);
376 (void) write(f, &nowbias, sizeof(nowbias));
377 (void) alarm(0);
378 }
なので inetd(8) も移植されてるけどだいぶ改変されているようである。
8 /*
9 * Based on "@(#)inetd.c 1.1 (ULTRIX) 4/11/85";
10 * and "@(#)inetd.c 5.1 (Berkeley) 5/28/85";
11 */
...
15 /*-----------------------------------------------------------------------
16 * Modification History
17 *
18 * 4/10/85 -- jrs
19 * Clean up little nits in code. Also add timer to select
20 * call so we can momentarily ignore multithreaded datagram
21 * connections in order to give the server time to pick up
22 * the initial packet before we try to listen to the socket again.
23 *
24 * Based on 4.2BSD labeled:
25 * inetd.c 4.2 84/05/18
26 *
27 *-----------------------------------------------------------------------
28 */
前述のとおり inetd(8) は 4.3BSD からなので
Based on 4.2BSD
というのはリリース前の内部バージョンがベースかね、実際 4.3BSD の inetd.c より何世代も古い。
14 static char sccsid[] = "@(#)inetd.c 5.6 (Berkeley) 4/29/86";
sccsid
って何?って人は存在しない
過去記事
で説明済だから精神高めて第三の目チャクラを開きアカシックレコードにアクセスすることで読んでくれ。
Time Protocol の現状
完全に歴史の遺物だし、今ではデフォルト無効だしいまさら有効にする意味も無い。
- 符号無し 32bit なので 2036 年以降使えない
- 精度が秒単位でしかない
- ネットワークの遅延や到達性そして冗長性などを一切考慮していない
- 原始的なクライアントサーバモデルでナウな分散制御システムではない
- rdate(8) で settimeofday(2) を使ってると刻は過去に戻ることがあるよ
これで使いたいやつおりゅ?
次回
4.3BSD で実装されたより優れた時刻同期システム timed(8) を解説するよ、たぶん。