📜 ⬆️ ⬇️

We write a simple NTP client

Hello, habrayuzer. Today I want to talk about how to write a simple NTP client. Basically, the conversation will go about the structure of the package and how to handle the response from the NTP server. The code will be written in python, because it seems to me that the best language for such things is simply not to be found. Connoisseurs will pay attention to the similarity of the code with the ntplib code - I was “inspired” by them.

So what is NTP in general? NTP is a protocol for interacting with time servers. This protocol is used in many modern machines. For example, the w32tm service in windows.

There are 5 versions of the NTP protocol. The first, version 0 (1985, RFC958)), is currently considered obsolete. The newer ones are used now, 1st (1988, RFC1059), 2nd (1989, RFC1119), 3rd (1992, RFC1305) and 4th (1996, RFC2030). 1-4 versions are compatible with each other, they differ only in the algorithms of the servers.

Package format



')
Leap indicator (correction indicator) - a number indicating a warning of a second coordination. Value:


Version number — The version number of the NTP protocol (1-4).

Mode (mode) - the mode of operation of the sender of the packet. A value from 0 to 7, the most frequent:


Stratum (layering level) - the number of intermediate layers between the server and the reference clock (1 - the server takes data directly from the reference clock, 2 - the server takes data from the server with level 1, etc.).
Poll is a signed integer representing the maximum interval between consecutive messages. Here, the NTP client indicates the interval at which it intends to poll the server, and the NTP server indicates the interval at which it assumes that it is polled. The value is the binary logarithm of seconds.
Precision is a signed integer representing the accuracy of the system clock. The value is the binary logarithm of seconds.
Root delay (server delay) - the time for which the clock reaches the NTP server, as the number of seconds with a fixed point.
Root dispersion - the spread of the NTP server clock as the number of seconds with a fixed point.
Ref id (source identifier) ​​- id hours. If the server has stratum 1, then ref id is the name of the atomic clock (4 ASCII characters). If the server uses a different server, then the address of this server is recorded in the ref id.
The last 4 fields are time - 32 bits - the whole part, 32 bits - the fractional part.
Reference - the latest clock reading on the server.
Originate - the time when the package was sent (filled by the server - more on that below).
Receive - the time the packet was received by the server.
Transmit - the time when the packet is sent from the server to the client (filled in by the client, this is also discussed below)

The last two fields will not be considered.

We will write our package:

Package code
class NTPPacket: _FORMAT = "!BB bb 11I" def __init__(self, version_number=2, mode=3, transmit=0): # Necessary of 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 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 must be able to turn it into an array of bytes.
For this (and inverse) 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)) 


To select the fractional part of the number to write to the package, we need the function get_fraction ():
get_fraction ()
 def get_fraction(number, precision): return int((number - int(number)) * 2 ** precision) 


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 lazy people, as an application, a code that turns a package into a beautiful 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) 


Send package to server


To the server, you need to send a packet with the filled fields Version , Mode and Transmit . In Transmit, you must specify the current time on the local machine (the number of seconds from January 1, 1900), the version is any of 1-4, the mode is 3 (client mode).

The server, accepting the request, fills all fields in the NTP packet by copying the value from the Transmit that came in the request into the Originate field. It is a mystery to me why a client cannot immediately fill in the value of his time in the Originate field. As a result, when a packet comes back, the client has 4 time values ​​- the time of sending the request ( Originate ), the time of receiving the request by the server ( Receive ), the time of sending the response by the server ( Transmit ) and the time of receiving the answer by the client - Arrive (not in the package). With these values ​​we can set the correct time.

Package send and receive code
 # 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 the English gentleman from the old task of Raymond M. Smallian (1978): “One person did not have watches, but there were exact wall clocks hanging at home, which he sometimes forgot to wind up. Once, having forgotten to start the clock again, he went to visit his friend, spent that evening, and when he returned home, he managed to set the clock correctly. How did he manage to do this if the travel time was not known in advance? ”The answer is:“ When you leave the house, the person starts the clock and remembers what position the hands are in. Coming to a friend and leaving the guests, he marks the time of his coming and departure. This allows him to know how much he was visiting. Returning home and looking at the clock, a person determines the duration of his absence. Subtracting from this time the time that he spent at a party, a person finds out the time spent on the road back and forth. By adding half the time spent on the road to the time he leaves the guests, he gets an opportunity to find out the time he came home and translate the hands of his watch accordingly. ”

Find the time the server works on the request:

  1. Find the path time of the packet from client to server: ((Arrive - Origin) - (Transmit - Receive)) / 2
  2. Find the difference between client and server time:
    Receive - Originate - ((Arrive - Originate) - (Transmit - Receive)) / 2 =
    2 * Receive - 2 * Origin - Arrive + Origin + Transmit - Receive =
    Receive - Originate - Arrive + Transmit

Add the resulting value to the local time and enjoy life.

Output
 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 link .

Source: https://habr.com/ru/post/448060/


All Articles