Wir schreiben einen einfachen NTP-Client.

Hallo, Habr-Nutzer. Heute möchte ich darĂŒber sprechen, wie man einen einfachen NTP-Client schreibt. Es wird hauptsĂ€chlich um die Struktur des Pakets und die Art der Verarbeitung der Antwort vom NTP-Server gehen. Der Code wird in Python geschrieben, denn ich glaube, dass es keine bessere Sprache fĂŒr solche Dinge gibt. Kenner werden die Ähnlichkeit des Codes mit dem Code von ntplib bemerken — ich habe mich genau davon "inspirieren lassen".

Was ist ĂŒberhaupt NTP? NTP ist ein Protokoll zur Interaktion mit Zeitservern. Dieses Protokoll wird in vielen modernen GerĂ€ten verwendet. Zum Beispiel der Dienst w32tm in Windows.

Es gibt insgesamt 5 Versionen des NTP-Protokolls. Die erste, die 0. Version (1985, RFC958), wird derzeit als veraltet angesehen. Momentan werden neuere Versionen verwendet, nÀmlich die 1. (1988, RFC1059), die 2. (1989, RFC1119), die 3. (1992, RFC1305) und die 4. (1996, RFC2030). Die Versionen 1-4 sind miteinander kompatibel, sie unterscheiden sich lediglich in den verwendeten Algorithmen. Server.

Paketformat

Wir schreiben einen einfachen NTP-Client.

Leap indicator (Korrekturindikator) — eine Zahl, die eine Warnung bezĂŒglich der Koordinatensekunde anzeigt. Bedeutung:

  • 0 – keine Korrektur
  • 1 – die letzte Minute des Tages enthĂ€lt 61 Sekunden
  • 2 – die letzte Minute des Tages enthĂ€lt 59 Sekunden
  • 3 – Serverfehler (Zeit nicht synchronisiert)

Version number (Versionsnummer) – die Versionsnummer des NTP-Protokolls (1-4).

Mode (Modus) — Arbeitsmodus des Paketversenders. Werte von 0 bis 7, die hĂ€ufigsten:

  • 3 – Client
  • 4 – Server
  • 5 – Broadcast-Modus

Stratum (Überlagerungsebene) – die Anzahl der Zwischenebenen zwischen dem Server und den Referenzuhren (1 – der Server erhĂ€lt Daten direkt von den Referenzuhren, 2 – der Server erhĂ€lt Daten von einem Server mit Ebene 1 usw.).
Pool — eine vorzeichenbehaftete ganze Zahl, die das maximale Intervall zwischen aufeinanderfolgenden Nachrichten darstellt. Der NTP-Client gibt hier das Intervall an, in dem er den Server abfragen möchte, wĂ€hrend der NTP-Server das Intervall angibt, in dem er erwartet, abgefragt zu werden. Der Wert entspricht dem binĂ€ren Logarithmus der Sekunden.
Precision (PrĂ€zision) — eine vorzeichenbehaftete ganze Zahl, die die Genauigkeit der Systemuhren darstellt. Der Wert entspricht dem binĂ€ren Logarithmus der Sekunden.
Root delay (Serververzögerung) – die Zeit, die benötigt wird, damit die Uhrzeit den NTP-Server erreicht, ausgedrĂŒckt in Sekunden mit fester Nachkommastelle.
Root dispersion (Messabweichung des Servers) — die Abweichung der Uhrzeit des NTP-Servers, ausgedrĂŒckt in Sekunden mit fester Nachkommastelle.
Ref id (Quell-ID) – id der Uhr. Wenn der Server Stratum 1 hat, ist die Ref-ID der Name der Atomuhren (4 ASCII-Zeichen). Wenn der Server einen anderen Server verwendet, wird die Adresse dieses Servers in der Ref-ID aufgezeichnet.
Die letzten 4 Felder stellen die Uhrzeit dar – 32 Bit – der ganzzahlige Teil, 32 Bit – der Nachkommenteil.
Referenz — die letzten Ablesewerte der Uhr auf dem Server.
Originate – die Zeit, zu der das Paket gesendet wurde (wird vom Server ausgefĂŒllt – dazu weiter unten mehr).
Receive – die Zeit, zu der das Paket vom Server empfangen wurde.
Transmit – die Zeit, zu der das Paket vom Server an den Client gesendet wurde (wird vom Client ausgefĂŒllt, dazu ebenfalls mehr unten).

Die letzten beiden Felder werden wir nicht betrachten.

Wir erstellen unser Paket:

Paketcode

class NTPPacket:
    _FORMAT = "!B B b b 11I"

    def __init__(self, version_number=2, mode=3, transmit=0):
        # Notwendig fĂŒr den Eintritt in die Schaltsekunde (2 Bits)
        self.leap_indicator = 0
        # Version des Protokolls (3 Bits)
        self.version_number = version_number
        # Modus des Absenders (3 Bits)
        self.mode = mode
        # Die Ebene des "Schichtens" zur Zeitmessung (1 Byte)
        self.stratum = 0
        # Intervall zwischen Anfragen (1 Byte)
        self.pool = 0
        # PrÀzision (log2) (1 Byte)
        self.precision = 0
        # Intervall bis die Uhr den NTP-Server erreicht (4 Bytes)
        self.root_delay = 0
        # Streuung der Uhr des NTP-Servers (4 Bytes)
        self.root_dispersion = 0
        # Indikator der Uhren (4 Bytes)
        self.ref_id = 0
        # Letzte Aktualisierungszeit auf dem Server (8 Bytes)
        self.reference = 0
        # Zeit des Sendens des Pakets von der lokalen Maschine (8 Bytes)
        self.originate = 0
        # Zeit des Empfangs auf dem Server (8 Bytes)
        self.receive = 0
        # Zeit des Sendens der Antwort vom Server (8 Bytes)
        self.transmit = transmit

Um ein Paket an den Server zu senden (und zu empfangen), mĂŒssen wir in der Lage sein, es in ein Byte-Array zu verwandeln.
FĂŒr diese (und die umgekehrte) Operation schreiben wir zwei Funktionen – pack() und unpack():

Funktion pack

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

Funktion unpack

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 Faulpelze, als Anhang – Code, der das Paket in eine schöne Zeichenkette umwandelt

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)

Paket an den Server senden

Ein Paket mit ausgefĂŒllten Feldern muss an den Server gesendet werden Version, Mode und Transmit. In Transmit Es ist notwendig, die aktuelle Zeit auf der lokalen Maschine anzugeben (Anzahl der Sekunden seit dem 1. Januar 1900), Version – beliebige aus 1-4, Modus – 3 (Client-Modus).

Der Server, der die Anfrage empfĂ€ngt, fĂŒllt alle Felder im NTP-Paket aus, indem er die Werte aus dem Feld Originate das aus der Anfrage stammt, ĂŒbertrĂ€gt. Transmit, bleibt mir ein RĂ€tsel, warum der Client den Wert seiner Zeit nicht sofort im Feld OriginateausfĂŒllen kann. Wenn das Paket schließlich zurĂŒckkommt, hat der Client 4 Zeitwerte – die Zeit des Anfrageversands (Originate), die Zeit, zu der die Anfrage vom Server empfangen wurde (Receive), die Zeit des Antwortversands durch den Server (Transmit) und die Zeit des Antwortempfangs durch den Client – Ankunft (nicht im Paket). Mit diesen Werten können wir die korrekte Zeit festlegen.

Code zum Senden und Empfangen des Pakets

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

Verarbeitung der Daten vom Server

Die Datenverarbeitung vom Server Ă€hnelt den Aktionen eines englischen Gentlemen aus der alten Aufgabe von Raymond M. Smullyan (1978): „Ein Mann hatte keine Armbanduhr, aber zu Hause hing eine genaue Wanduhr, die er manchmal vergaß zu stellen. Eines Tages, als er die Uhr wieder einmal nicht gestellt hatte, ging er zu einem Freund zu Besuch, verbrachte dort den Abend und schaffte es, zu Hause die Uhr richtig zu stellen. Wie gelang es ihm, das zu tun, wenn die Reisezeit im Voraus nicht bekannt war?“ Die Antwort lautet: „Als er das Haus verließ, stellte der Mann die Uhr und merkte sich, in welcher Position die Zeiger standen. Bei seinem Freund angekommen und beim Verlassen des Hauses notierte er die Zeit seines Kommens und Gehens. Das ermöglicht es ihm zu erfahren, wie lange er zu Besuch war. Bei seiner RĂŒckkehr nach Hause und einem Blick auf die Uhr kann der Mann die Dauer seiner Abwesenheit bestimmen. Indem er von dieser Zeit die Zeit abzieht, die er bei seinem Freund verbracht hat, erfĂ€hrt er die fĂŒr den Hin- und RĂŒckweg erforderliche Zeit. Er addiert die halbe Zeit, die er fĂŒr die Reise benötigt hat, zur Zeit des Verlassens des Hauses und kann so die Ankunftszeit zu Hause ermitteln und die Zeiger seiner Uhr entsprechend einstellen.“

Wir finden die Zeit, die der Server fĂŒr die Bearbeitung der Anfrage benötigt:

  1. Wir finden die Zeit, die das Paket vom Client zum Server benötigt: ((Ankommen – Abfliegen) — (Übertragen – Empfangen)) / 2
  2. Wir finden die Differenz zwischen der Zeit des Clients und des Servers:
    Empfangen — Abfliegen — ((Ankommen – Abfliegen) — (Übertragen – Empfangen)) / 2 =
    2 * Empfangen — 2 * Abfliegen – Ankommen + Abfliegen + Übertragen – Empfangen =
    Empfangen – Abfliegen – Ankommen + Übertragen

Wir addieren den erhaltenen Wert zur lokalen Zeit und freuen uns ĂŒber das Leben.

Ergebnis anzeigen

time_different = answer.get_time_different(arrive_time)
result = "Zeitunterschied: {}\nServerzeit: {}\n{}".format(
    time_different,
    datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
    answer.to_display())
print(result)

NĂŒtzlich Link.

Quelle: habr.com

ZuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz kaufen, VPS VDS Server đŸ”„ ZuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz kaufen, VPS VDS Server - ProHoster