Wir schreiben einen einfachen NTP-Client

Hallo, Habr-Nutzer. Heute möchte ich Ihnen zeigen, wie Sie Ihren eigenen einfachen NTP-Client schreiben können. Dabei wird es hauptsĂ€chlich um die Paketstruktur und die Verarbeitung der Antwort vom NTP-Server gehen. Der Code wird in Python geschrieben, da ich der Meinung bin, dass es keinen besseren Sprach fĂŒr solche Anwendungen gibt. Experten werden die Ähnlichkeit des Codes mit dem von ntplib bemerken – ich habe mich davon 'inspirieren' lassen.

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

Es gibt insgesamt 5 Versionen des NTP-Protokolls. Die erste, die 0. Version (1985, RFC958), gilt mittlerweile als veraltet. Aktuell werden neuere Versionen verwendet: die 1. (1988, RFC1059), die 2. (1989, RFC1119), die 3. (1992, RFC1305) und die 4. (1996, RFC2030). Die Versionen 1-4 sind miteinander kompatibel, unterscheiden sich jedoch nur in den Algorithmen. Server.

Paketformat

Wir schreiben einen einfachen NTP-Client

Leap-Indicator (Korrektur-Indikator) – eine Zahl, die eine Warnung ĂŒber eine Schaltsekunde 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 – Serverausfall (Uhrzeit nicht synchronisiert)

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

Modus (Modus) – Betriebsmodus des Senders. Werte von 0 bis 7, hĂ€ufigste sind:

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

Stratum (Schicht) – Anzahl der Zwischenebenen zwischen dem Server und der Referenzuhr (1 – Server bezieht Daten direkt von der Referenzuhr, 2 – Server bezieht Daten von einem Server der Stufe 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 anfragen möchte, wĂ€hrend der NTP-Server angibt, in welchem Intervall er erwartet, angefragt zu werden. Der Wert entspricht dem binĂ€ren Logarithmus der Sekunden.
Genauigkeit (Genauigkeit) – eine vorzeichenbehaftete ganze Zahl, die die Genauigkeit der Systemuhr reprĂ€sentiert. Der Wert entspricht dem binĂ€ren Logarithmus der Sekunden.
Root-Verzögerung (Serververzögerung) – die Zeit, die benötigt wird, bis die Uhrzeit den NTP-Server erreicht, als Anzahl von Sekunden mit fester Dezimalstelle.
Root-Dispersion (Streuung der Serverwerte) — die Abweichung der NTP-Serverzeit in Sekunden mit fester Dezimalstelle.
Ref id (Quellenidentifikator) – ID der Uhr. Wenn der Server Stratum 1 hat, ist der 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 gespeichert.
Die letzten 4 Felder reprĂ€sentieren die Zeit – 32 Bit – ganzzahlig, 32 Bit – nach dem Komma.
Referenz — die letzten Uhrzeitdaten des Servers.
Originate – die Uhrzeit, zu der das Paket gesendet wurde (wird vom Server ausgefĂŒllt – dazu spĂ€ter mehr).
Receive – die Uhrzeit, 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 spĂ€ter).

Die beiden letzten Felder lassen wir unberĂŒcksichtigt.

Schreiben wir unser Paket:

Paketcode

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

    def __init__(self, versionsnummer=2, modus=3, transmit=0):
        # Notwendig fĂŒr den Eintritt in die Schaltsekunde (2 Bits)
        self.leap_indicator = 0
        # Protokollversion (3 Bits)
        self.versionsnummer = versionsnummer
        # Modus des Senders (3 Bits)
        self.modus = modus
        # Der Grad der "Schichtung" der Zeitmessung (1 Byte)
        self.stratum = 0
        # Intervall zwischen Anfragen (1 Byte)
        self.pool = 0
        # PrÀzision (log2) (1 Byte)
        self.prÀzision = 0
        # Intervall fĂŒr die Uhrzeit bis zum NTP-Server (4 Bytes)
        self.root_delay = 0
        # Streuung der Uhr NTP-Server (4 Bytes)
        self.root_dispersion = 0
        # Kennung 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 Pakete an den Server zu senden (und zu empfangen), mĂŒssen wir in der Lage sein, sie in ein Byte-Array umzuwandeln.
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))

Die unpack-Funktion

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 Faulenzer gibt es als Bonus – einen Code, der das Paket in eine schöne Zeichenkette umwandelt.

def to_display(self):
        return "Sprungindikator: {0.leap_indicator}n" 
                "Versionsnummer: {0.version_number}n" 
                "Modus: {0.mode}n" 
                "Stratum: {0.stratum}n" 
                "Pool: {0.pool}n" 
                "PrÀzision: {0.precision}n" 
                "Root-Verzögerung: {0.root_delay}n" 
                "Root-Streuung: {0.root_dispersion}n" 
                "Ref-ID: {0.ref_id}n" 
                "Referenz: {0.reference}n" 
                "Ursprung: {0.originate}n" 
                "Empfangen: {0.receive}n" 
                "Übertragen: {0.transmit}"
                .format(self)

Paket an den Server senden

Es muss ein Paket mit ausgefĂŒllten Feldern an den Server gesendet werden Version, Modus und TransmitIn Transmit Es muss die aktuelle Zeit auf dem lokalen Computer angegeben werden (Anzahl der Sekunden seit dem 1. Januar 1900), die Version – beliebig von 1-4, Modus – 3 (Client-Modus).

Der Server fĂŒllt auf den NTP-Paket alle Felder aus, indem er den Wert aus dem Feld kopiert Originate aus Transmit, das in der Anfrage angekommen ist. Es ist mir ein RĂ€tsel, warum der Client den Wert seiner Zeit nicht sofort im Feld eintragen kann Originate. Wenn das Paket zurĂŒckkommt, hat der Client 4 Zeitwerte – die Zeit des Anfrageversands (Originate), die Zeit des Anfrageeingangs beim Server (Receive), die Zeit der AntwortĂŒbertragung durch den Server (Transmit) und die Zeit des Antwortempfangs durch den Client – Ankommen (nicht im Paket). Mit diesen Werten können wir die korrekte Zeit einstellen.

Versand- und Empfangscode 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)

Datenverarbeitung vom Server

Die Datenverarbeitung vom Server Ă€hnelt den Handlungen eines englischen Gentlemen aus einer alten Aufgabe von Raymond M. Smullyan (1978): „Ein Mann hatte keine Armbanduhr, dafĂŒr hingen in seinem Haus genaue Wanduhr, die er manchmal vergaß aufzuziehen. Eines Tages, als er wieder einmal vergaß, die Uhr aufzuziehen, besuchte er einen Freund, verbrachte dort den Abend und konnte, als er nach Hause zurĂŒckkehrte, die Uhr richtig einstellen. Wie gelang ihm das, wenn die Reisezeit vorher nicht bekannt war?“ Die Antwort lautet: „Als er das Haus verließ, zog der Mann die Uhr auf und merkte sich die Position der Zeiger. Bei seinem Freund angekommen und beim Verlassen ihn merkte er die Zeiten seines Kommens und Gehens. Dies ermöglicht es ihm zu erfahren, wie lange er zu Besuch war. Wenn er nach Hause kommt und auf die Uhr schaut, bestimmt er die Dauer seiner Abwesenheit. Indem er die Zeit, die er bei seinem Freund verbracht hat, von dieser Zeit abzieht, erfĂ€hrt er die fĂŒr Hin- und RĂŒckweg benötigte Zeit. Wenn er zur Zeit des Verlassens des Gastes die HĂ€lfte der benötigten Reisezeit hinzufĂŒgt, kann er die Ankunftszeit zu Hause herausfinden und die Zeiger seiner Uhr entsprechend einstellen.“

Wir ermitteln die Serverlaufzeit fĂŒr die Anfrage:

  1. Wir bestimmen die Paketlaufzeit vom Client zum Server: ((Ankommen – UrsprĂŒnglich) — (Übertragen – Empfangen)) / 2
  2. Wir finden die Differenz zwischen der Client- und Serverzeit:
    Empfangen — UrsprĂŒnglich — ((Ankommen – UrsprĂŒnglich) — (Übertragen – Empfangen)) / 2 =
    2 * Empfangen — 2 * UrsprĂŒnglich – Ankommen + UrsprĂŒnglich + Übertragen – Empfangen =
    Empfangen – UrsprĂŒnglich – Ankommen + Übertragen

Wir fĂŒgen den erhaltenen Wert zur lokalen Zeit hinzu und freuen uns am Leben.

Ergebnis anzeigen

time_different = answer.get_time_different(arrive_time)
result = "Zeitdifferenz: {}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

Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server đŸ”„ Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster