Schreiben eines einfachen NTP-Clients

Hallo habrayuzery. Heute möchte ich darüber sprechen, wie ich meinen einfachen NTP-Client schreibe. Grundsätzlich werden wir über die Struktur des Pakets und die Verarbeitung der Antwort vom NTP-Server sprechen. Der Code wird in Python geschrieben, weil meiner Meinung nach die beste Sprache für solche Dinge einfach nicht gefunden werden kann. Kenner werden auf die Ähnlichkeit des Codes mit dem ntplib-Code achten - ich war davon „inspiriert“.

Was genau ist NTP? NTP - Protokoll der Interaktion mit genauen Zeitservern. Dieses Protokoll wird in vielen modernen Maschinen verwendet. Zum Beispiel der Dienst w32tm in Windows.

Insgesamt gibt es 5 Versionen des NTP-Protokolls. Die erste, 0. Version (1985, RFC958)) gilt derzeit als veraltet. Jetzt werden die neueren verwendet, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) und 4. (1996, RFC2030). 1-4 Versionen sind miteinander kompatibel, sie unterscheiden sich nur in den Serverbetriebsalgorithmen.

Paketformat




Sprungindikator (Korrekturindikator) - eine Zahl, die eine Warnung vor der zweiten Koordination anzeigt. Wert:

  • 0 - keine Korrektur
  • 1 - Die letzte Minute des Tages enthält 61 Sekunden
  • 2 - Die letzte Minute des Tages enthält 59 Sekunden
  • 3 - Serverstörung (Zeit nicht synchronisiert)

Versionsnummer - Die Versionsnummer des NTP-Protokolls (1-4).

Modus - Betriebsmodus des Paketsenders. Wert von 0 bis 7, am häufigsten:

  • 3 - Kunde
  • 4 - Server
  • 5 - Sendemodus

Schicht (Layering-Ebene) - Die Anzahl der Zwischenschichten zwischen dem Server und der Referenzuhr (1 - Der Server entnimmt Daten direkt von der Referenzuhr, 2 - Der Server nimmt Daten vom Server mit der Ebene 1 usw.).
Poll ist eine vorzeichenbehaftete Ganzzahl, die das maximale Intervall zwischen aufeinanderfolgenden Nachrichten darstellt. Hier gibt der NTP-Client das Intervall an, in dem er erwartet, den Server abzufragen, und der NTP-Server gibt das Intervall an, in dem er erwartet, abgefragt zu werden. Der Wert ist der binäre Logarithmus von Sekunden.
Präzision ist eine vorzeichenbehaftete Ganzzahl, die die Genauigkeit der Systemuhr darstellt. Der Wert ist der binäre Logarithmus von Sekunden.
Root-Verzögerung (Server-Verzögerung) - Die Zeit, während der die Uhr den NTP-Server erreicht, als Anzahl der Sekunden mit einem festen Punkt.
Root-Dispersion - Die Streuung der NTP-Serveruhr als Anzahl der Sekunden mit einem festen Punkt.
Ref id (Quellkennung) - ID der Uhr. Wenn der Server eine Schicht von 1 hat, ist ref id der Name der Atomuhr (4 ASCII-Zeichen). Wenn der Server einen anderen Server verwendet, wird die Adresse dieses Servers in die Referenz-ID geschrieben.
Die letzten 4 Felder sind Zeit - 32 Bit - der ganzzahlige Teil, 32 Bit - Bruchteil.
Referenz - die letzte Uhr auf dem Server.
Ursprung - der Zeitpunkt, zu dem das Paket gesendet wurde (vom Server ausgefüllt - mehr dazu weiter unten).
Empfangszeit, zu der das Paket vom Server empfangen wurde.
Sendezeit, zu der das Paket vom Server an den Client gesendet wurde (vom Client ausgefüllt, mehr dazu weiter unten).

Wir werden die letzten beiden Felder nicht berücksichtigen.

Schreiben wir unser Paket:

Paketcode
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 


Um ein Paket an den Server zu senden (und zu empfangen), müssen wir es in ein Array von Bytes umwandeln können.
Für diese (und umgekehrte) Operation schreiben wir zwei Funktionen - pack () und unpack ():

Packfunktion
 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)) 


Um den Bruchteil der Zahl zum Schreiben in das Paket auszuwählen, benötigen wir die Funktion get_fraction ():
get_fraction ()
 def get_fraction(number, precision): return int((number - int(number)) * 2 ** precision) 


Funktion auspacken
 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 


Für faule Leute als Anwendung - ein Code, der ein Paket in eine schöne Zeichenfolge verwandelt
 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) 


Senden eines Pakets an den Server


Es ist erforderlich, ein Paket mit den ausgefüllten Feldern Version , Modus und Senden an den Server zu senden. In Senden müssen Sie die aktuelle Zeit auf dem lokalen Computer (Anzahl der Sekunden seit dem 1. Januar 1900), Version - beliebig 1-4, Modus - 3 (Client-Modus) angeben.

Nachdem der Server die Anforderung akzeptiert hat, füllt er alle Felder im NTP-Paket aus, indem er den Übertragungswert aus der Anforderung in das Feld Originate kopiert. Es ist mir ein Rätsel, warum der Kunde den Wert seiner Zeit nicht sofort in das Feld " Ursprung" eingeben kann. Wenn das Paket zurückkommt, hat der Client viermal - die Zeit, zu der die Anforderung gesendet wurde ( Originate ), die Zeit, zu der der Server die Anforderung empfangen hat ( Receive ), die Zeit, zu der der Server die Antwort gesendet hat ( Transmit ) und die Zeit, zu der der Client die Antwort empfangen hat - Arrive (nicht im Paket). Mit diesen Werten können wir die richtige Zeit einstellen.

Paket senden und empfangen 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) 


Daten vom Server verarbeiten


Die Verarbeitung von Daten vom Server ähnelt den Aktionen des englischen Gentleman aus der alten Aufgabe von Raymond M. Sullian (1978): „Eine Person hatte keine Uhr, aber andererseits gab es eine genaue Wanduhr, die er manchmal vergessen hatte zu starten. Einmal, nachdem er vergessen hatte, die Uhr wieder zu starten, besuchte er seinen Freund, verbrachte den Abend an diesem Ort und als er nach Hause zurückkehrte, gelang es ihm, die Uhr richtig einzustellen. Wie hat er das geschafft, wenn die Reisezeit nicht im Voraus bekannt war? “ Die Antwort lautet: „Eine Person verlässt das Haus, startet die Uhr und merkt sich, in welcher Position sich die Zeiger befinden. Als er zu einem Freund kommt und die Gäste verlässt, notiert er die Zeit seiner Ankunft und Abreise. Auf diese Weise kann er herausfinden, wie viel er besucht hat. Nach der Rückkehr nach Hause und dem Blick auf die Uhr bestimmt eine Person die Dauer ihrer Abwesenheit. Wenn man von dieser Zeit die Zeit abzieht, die er für einen Besuch aufgewendet hat, lernt eine Person die Zeit, die sie für die Hin- und Herreise aufgewendet hat. Nachdem er die Hälfte der für die Reise aufgewendeten Zeit zur Zeit des Verlassens der Gäste hinzugefügt hat, hat er die Möglichkeit, die Ankunftszeit zu Hause herauszufinden und die Uhrzeiger entsprechend zu übersetzen. “

Wir finden die Serverarbeitszeit auf der Anfrage:

  1. Wir finden die Paketpfadzeit vom Client zum Server: ((Arrive - Originate) - (Transmit - Receive)) / 2
  2. Finden Sie den Unterschied zwischen Client- und Serverzeit:
    Empfangen - Originieren - ((Ankommen - Originieren) - (Senden - Empfangen)) / 2 =
    2 * Empfangen - 2 * Originieren - Ankommen + Originieren + Senden - Empfangen =
    Empfangen - Ursprünglich - Ankommen + Senden

Fügen Sie den erhaltenen Wert zur Ortszeit hinzu und genießen Sie das Leben.

Ausgabeergebnis
 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) 


Nützlicher Link .

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


All Articles