seriot.ch

About | Projects | Trail

A Tiny NTP client

July 20th, 2015

Feel free to comment on reddit or hacker news.

Table of Contents

  1. Introduction
  2. Minimal one liner NTP client
    2.1 Build the NTP datagram
    2.2 Send the NTP datagram
    2.3 Extract the NTP timestamp
    2.4 Convert the NTP timestamp to UNIX timestamp
    2.5 Print the actual date and time
  3. Conclusion
  4. Bonus: a TP client

1. Introduction

Network Time Protocol (NTP) is a venerable network protocol used to synchronize clocks among networked computers. NTP is used by most Unix systems, in particular by iOS and OS X.

I was wondering if it was possible to query an NTP server and display the actual time with a very short command that would fit in a tweet, without using an actual NTP client or library of course. The main objective was not to write a readable or maintainable program, but write the sortest working one I could find.

2. Minimal one liner NTP client

I came up with this Bash one-liner, written in only 87 characters.

$ date -r$((16#`printf "xb%-47.s"|nc -uw1 ntp.metas.ch 123|xxd -s40 -l4 -p`-2208988800))

[Update 2015-07-22] Pete Forman shrunk this solution to 79 characters. I've updated this article accordingly.

$ date -r$((0x`printf c%47s|nc -uw1 ntp.metas.ch 123|xxd -s40 -l4 -p`-64#23GDW0))

This command builds a NTP query, sends it through UDP to a time server, extracts a timestamp from the response and uses it to display the actual local time, such as:

Sat Jul 18 15:53:46 CEST 2015

Strictly speaking, this program is far from being an real NTP client, since it doesn't implement the whole NTP specification. However, it can tell you the time! Now, here is a dissection of this command and an explanation on how it works.

[Update 2015-07-21] As noted by several readers, my command assumes the *BSD version of date. GNU date requires -d@ instead of -r:

$ date -d@$((0x`printf c%47s|nc -uw1 ntp.metas.ch 123|xxd -s40 -l4 -p`-64#23GDW0))

Last thing, I used ntp.metas.ch instead of pool.ntp.org because I wanted to post this one-liner in a tweet, and Twitter would automatically replace .org domains with a stupid bit.ly shortener, hence making the command unreadable.

2.1 Build the NTP datagram

printf c%47s

The NTP protocol can be seen as a client server protocol. The client can send the following datagram trough UDP port 123 and expect a response with the same structure:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|LI | VN  |Mode |    Stratum     |     Poll      |  Precision   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Root Delay                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Root Dispersion                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          Reference ID                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                     Reference Timestamp (64)                  +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                      Origin Timestamp (64)                    +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                      Receive Timestamp (64)                   +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                      Transmit Timestamp (64)                  +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

In a minimal request, fill the version number and the mode is enough. We can use version number VN = 4 (100) and mode = "client" = 3 (011), which gives 0010 0011 = 0x33.

In practice, NTP servers still support version 1 of the protocol (001), so we can also use 0000 1011 = 0xb and so one character is spared in our final command.

Even better, Pete Forman proposed to use a byte that is also a printable ASCII character.

'c' == 0x63 == 0110 0011 stands for:

 0 1 2 3 4 5 6 7 
+-+-+-+-+-+-+-+-+
|LI | VN  |Mode |
+-+-+-+-+-+-+-+-+
 0 1 1 0 0 0 1 1

printf "c%47s" pads the '0x63 byte with 47 spaces 0x20 and doesn't output an extraneous newline. For sure, it would be cleaner to change the spaces into zeros with tr:

printf "c%47s" | tr ' ' ''

but in practice the server does interpret our request correctly, so we'll spare a few more bytes here.

2.2 Send the NTP datagram

nc -uw1 ntp.metas.ch 123

Netcat with -u sends the input bytes as UDP packets on port 123 (the NTP port) to the server ntp.metas.ch from the Swiss Federal Institute of Metrology (METAS). The -w1 option sets a timeout of 1 second, so that the connection is closed and the response can be processed.

We may probably find an NTP server with a shorter host name and come up with a shorter command.

Here is a sample response datagram:

$ printf "c%47s"|nc -uw1 ntp.metas.ch 123|xxd -c4 -g4
0000000: 0c0120ed  .. .
0000004: 00000000  ....
0000008: 0000001f  ....
000000c: 50505300  PPS.
0000010: d957db44  .W.D
0000014: 202f5abf   /Z.
0000018: 20202020      
000001c: 20202020      
0000020: d957db53  .W.S
0000024: a7e8fb7f  ....
0000028: d957db53  .W.S # timestamp here
000002c: a7eacfa2  ....

2.3 Extract the NTP timestamp

xxd -s40 -l4 -p

We want to read the "Transmit Timestamp" field, defined as the "time at the server when the response left for the client, in NTP timestamp format".

According to the RFC, 64 bits NTP timestamps are made of "a 32-bit unsigned seconds field spanning 136 years and a 32-bit fraction field resolving 232 picoseconds", the epoch being "0 h 1 January 1900 UTC".

It means that we have to read bytes 41,42,43,44 ie in the previous example d9 57 db 53. xxd uses option -s40 to skip 40 bytes, -l4 to read 4 bytes and -p to output the raw result unformatted.

2.4 Convert the NTP timestamp to UNIX timestamp

$((0xd957db53 - 64#23GDW0))

Bash will evaluate integer arithmetic expressions written inside $(( )). What we do here is convert the NTP timestamp into UNIX timestamp by substracting the number of seconds between 1900 and 1980, ie 2208988800.

We use the 0x notation before the NTP timestamp since it is to be read in base 16, as required by xxd output.

Pete Forman proposed to encode the Epoch in base 64 as 64#23GDW0. 1 more character spared over its decimal representation.

2.5 Print the actual date and time

date -r 1437425990

date will print the local date and time. The -r option reads the number of seconds since Epoch 00:00:00 UTC, January 1, 1970.

date -r is the BSD flavor. The GNU one date -d@ requires one more character, though.

3. Conclusion

Unix tools can be extremely powerful, useful and concise. They are worth considering when the first reflex may be to use high level languages such as Python for instance.

4. Bonus: a TP client

Here is a variant using an ancestor of NTP named Time Protocol. Written in 1983, the protocol is dead simple:

When used via UDP the time service works as follows:
    S: Listen on port 37 (45 octal).
    U: Send an empty datagram to port 37.
    S: Receive the empty datagram.
    S: Send a datagram containing the time as a 32 bit binary number.
    U: Receive the time datagram.

While lacking NTP synchronization capabilities, this protocol can still give the time, and it's not hard to find TP servers still operating.

Our minimal bash program can now be written in only 61 characters:

$ date -r$((0x`echo|nc -uw1 ntp.metas.ch 37|xxd -p`-64#23GDW0))

We changed printf into echo which is shorter and sends a single newline character to the server. Also, we don't need to tell xxd to jump to some offset and limit the number of bytes it has to read, since the response consists in nothing more that the timestamp.

[Update 2015-07-21] Also, thanks to jmtd on reddit for noticing that we can spare 3 characters by replacing echo "" with echo.