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 のサービス側の実装。

プロトコルはクソほど単純で

  1. サービスはポート 37 で待つ
  2. クライアントはポート 37 に接続
  3. サービスは時刻を 1900-01-01 00:00:00 GMT からの経過秒数として 32bit binary で返す
  4. エンディアン? エイプリルフールジョーク にでも従っておいて

マジでこんだけ。

実際試してみよっか、/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) を解説するよ、たぶん。