Hello, Habr users. Today I want to talk about how to write a simple NTP client. The discussion will mainly focus on the packet structure and how to handle the response from the NTP server. The code will be written in Python, as I believe there is no better language for such tasks. Experts will notice the similarity of the code with ntplib; I was "inspired" by it.
So, what is NTP? NTP is a protocol for interacting with time servers. This protocol is used in many modern machines. For instance, the w32tm service in Windows.
There are a total of 5 versions of the NTP protocol. The first, version 0 (1985, RFC958), is currently considered outdated. Newer versions in use are 1 (1988, RFC1059), 2 (1989, RFC1119), 3 (1992, RFC1305), and 4 (1996, RFC2030). Versions 1-4 are compatible with each other; they differ only in their operating algorithms. servers.
Packet Format

Leap Indicator (correction indicator) β a number that warns about a leap second. Value:
- 0 β no correction
- 1 β the last minute of the day contains 61 seconds
- 2 β the last minute of the day contains 59 seconds
- 3 β server malfunction (time not synchronized)
Version Number (version number) β the protocol version number (1-4).
Mode (mode) β the operational mode of the packet sender. Value ranges from 0 to 7, most common:
- 3 β client
- 4 β server
- 5 β broadcast mode
Stratum (stratum level) β the number of intermediate layers between the server and the reference clocks (1 β the server gets data directly from the reference clocks, 2 β the server gets data from a level 1 server, etc.).
Pool β a signed integer representing the maximum interval between consecutive messages. The NTP client indicates here the interval at which it intends to poll the server, while the NTP server indicates the interval at which it expects to be polled. The value equals the binary logarithm of seconds.
Precision (precision) β a signed integer representing the accuracy of the system clock. The value equals the binary logarithm of seconds.
Root Delay (server delay) β the time it takes for clock readings to reach the NTP server, as a fixed-point number of seconds.
Root Dispersion (server reading dispersion) β the dispersion of the NTP server readings as a fixed-point number of seconds.
Ref Id (source identifier) β id of the clock. If the server has stratum 1, then ref id is the name of atomic clocks (4 ASCII characters). If the server uses another server, the address of that server is recorded in ref id.
The last 4 fields represent time β 32 bits β integer part, 32 bits β fractional part.
Reference β the last reading of the clock on the server.
Originate β the time when the packet was sent (filled in by the server β more on this below).
Receive β the time the packet was received by the server.
Transmit β the time of sending the packet from the server to the client (filled in by the client, more on this below).
We will not consider the last two fields.
Let's write our packet:
Packet Code
class NTPPacket:
_FORMAT = "!B B b b 11I"
def __init__(self, version_number=2, mode=3, transmit=0):
# Necessary to enter leap second (2 bits)
self.leap_indicator = 0
# Version of protocol (3 bits)
self.version_number = version_number
# Mode of sender (3 bits)
self.mode = mode
# The level of "layering" reading time (1 byte)
self.stratum = 0
# Interval between requests (1 byte)
self.pool = 0
# Precision (log2) (1 byte)
self.precision = 0
# Interval for the clock to reach NTP server (4 bytes)
self.root_delay = 0
# Scatter the clock NTP-server (4 bytes)
self.root_dispersion = 0
# Indicator of clocks (4 bytes)
self.ref_id = 0
# Last update time on server (8 bytes)
self.reference = 0
# Time of sending packet from local machine (8 bytes)
self.originate = 0
# Time of receipt on server (8 bytes)
self.receive = 0
# Time of sending answer from server (8 bytes)
self.transmit = transmit
To send (and receive) a packet to the server, we need to be able to convert it to a byte array.
For this (and the reverse) operation, we will write two functions β pack() and unpack():
Pack Function
def pack(self):
return struct.pack(NTPPacket._FORMAT,
(self.leap_indicator << 6) +
(self.version_number << 3) + self.mode,
self.stratum,
self.pool,
self.precision,
int(self.root_delay) + get_fraction(self.root_delay, 16),
int(self.root_dispersion) +
get_fraction(self.root_dispersion, 16),
self.ref_id,
int(self.reference),
get_fraction(self.reference, 32),
int(self.originate),
get_fraction(self.originate, 32),
int(self.receive),
get_fraction(self.receive, 32),
int(self.transmit),
get_fraction(self.transmit, 32))
Unpack Function
def unpack(self, data: bytes):
unpacked_data = struct.unpack(NTPPacket._FORMAT, data)
self.leap_indicator = unpacked_data[0] >> 6 # 2 bits
self.version_number = unpacked_data[0] >> 3 & 0b111 # 3 bits
self.mode = unpacked_data[0] & 0b111 # 3 bits
self.stratum = unpacked_data[1] # 1 byte
self.pool = unpacked_data[2] # 1 byte
self.precision = unpacked_data[3] # 1 byte
# 2 bytes | 2 bytes
self.root_delay = (unpacked_data[4] >> 16) +
(unpacked_data[4] & 0xFFFF) / 2 ** 16
# 2 bytes | 2 bytes
self.root_dispersion = (unpacked_data[5] >> 16) +
(unpacked_data[5] & 0xFFFF) / 2 ** 16
# 4 bytes
self.ref_id = str((unpacked_data[6] >> 24) & 0xFF) + " " +
str((unpacked_data[6] >> 16) & 0xFF) + " " +
str((unpacked_data[6] >> 8) & 0xFF) + " " +
str(unpacked_data[6] & 0xFF)
self.reference = unpacked_data[7] + unpacked_data[8] / 2 ** 32 # 8 bytes
self.originate = unpacked_data[9] + unpacked_data[10] / 2 ** 32 # 8 bytes
self.receive = unpacked_data[11] + unpacked_data[12] / 2 ** 32 # 8 bytes
self.transmit = unpacked_data[13] + unpacked_data[14] / 2 ** 32 # 8 bytes
return self
For the lazy, as an appendix β code that converts the packet into a nice string
def to_display(self):
return "Leap indicator: {0.leap_indicator}n"
"Version number: {0.version_number}n"
"Mode: {0.mode}n"
"Stratum: {0.stratum}n"
"Pool: {0.pool}n"
"Precision: {0.precision}n"
"Root delay: {0.root_delay}n"
"Root dispersion: {0.root_dispersion}n"
"Ref id: {0.ref_id}n"
"Reference: {0.reference}n"
"Originate: {0.originate}n"
"Receive: {0.receive}n"
"Transmit: {0.transmit}"
.format(self)
Sending the packet to the server
It is necessary to send a packet to the server with filled fields Version, Mode and Transmit. In Transmit It is required to specify the current time on the local machine (the number of seconds since January 1, 1900), the version β any of 1-4, mode β 3 (client mode).
The server, upon receiving the request, fills in all fields in the NTP packet by copying the value from Originate the value received in the request. It is a mystery to me why the client cannot immediately fill in its time value in the field Transmit. As a result, when the packet comes back, the client has 4 time values β the time the request was sent ( Originate), the time the request was received by the server (Originate), the time the response was sent by the server (Receive), and the time the response was received by the client βTransmitArrive (not in the packet). With these values, we can establish the correct time. Code for sending and receiving the packet
Processing data from the server
# Time difference between 1970 and 1900, seconds
FORMAT_DIFF = (datetime.date(1970, 1, 1) - datetime.date(1900, 1, 1)).days * 24 * 3600
# Waiting time for recv (seconds)
WAITING_TIME = 5
server = "pool.ntp.org"
port = 123
packet = NTPPacket(version_number=2, mode=3, transmit=time.time() + FORMAT_DIFF)
answer = NTPPacket()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.settimeout(WAITING_TIME)
s.sendto(packet.pack(), (server, port))
data = s.recv(48)
arrive_time = time.time() + FORMAT_DIFF
answer.unpack(data)
Processing data from the server
Data processing from the server is similar to the actions of an English gentleman in Raymond M. Smullyan's old riddle (1978): 'One man didn't have a wristwatch, but he did have accurate wall clocks at home, which he sometimes forgot to wind. One day, having forgotten to wind the clocks again, he went to visit his friend, spent the evening there, and when he returned home, he was able to correctly set the clock. How did he manage to do this, since the travel time was not known in advance?' The answer is: 'When leaving home, the man winds the clock and remembers the position of the hands. Upon arriving at his friend's house and leaving, he notes the time of his arrival and departure. This allows him to find out how long he stayed. Upon returning home and looking at the clock, he determines the duration of his absence. By subtracting the time he spent visiting from that duration, he learns the time taken for the round trip. By adding half of the travel time to the time of leaving his friend's, he is able to ascertain the time of his arrival home and adjust his clock hands accordingly.'
Finding the server's processing time for the request:
- Finding the round-trip time of the packet from client to server: ((Arrive β Originate) β (Transmit β Receive)) / 2
- Finding the difference between the client's and server's time:
Receive β Originate β ((Arrive β Originate) β (Transmit β Receive)) / 2 =
2 * Receive β 2 * Originate β Arrive + Originate + Transmit β Receive =
Receive β Originate β Arrive + Transmit
Add the obtained value to the local time and enjoy life.
Output the result
time_different = answer.get_time_different(arrive_time)
result = "Time difference: {}\nServer time: {}\n{}".format(
time_different,
datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
answer.to_display())
print(result)
Useful .
Source: habr.com
