Schreiwen engem einfachen NTP Client

Hallo Habrauser. Haut wëll ech schwätzen iwwer wéi Dir Ären eegene einfachen NTP Client schreift. Prinzipiell wäert d'Gespréich op d'Struktur vum Paket dréien a wéi d'Äntwert vum NTP-Server veraarbecht gëtt. De Code gëtt am Python geschriwwe ginn, well et menger Meenung no einfach keng besser Sprooch gëtt fir esou Saachen. Kenner wäerten oppassen op d'Ähnlechkeet vum Code mam ntplib Code - ech war "inspiréiert" dovun.

Also wat ass NTP iwwerhaapt? NTP ass e Protokoll fir mat Zäitserver ze kommunizéieren. Dëse Protokoll gëtt a ville modernen Maschinnen benotzt. Zum Beispill de w32tm Service op Windows.

Et gi 5 Versioune vum NTP Protokoll am Ganzen. Déi éischt Versioun 0 (1985, RFC958) gëtt de Moment als obsolet ugesinn. Méi nei ginn am Moment benotzt, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) a 4. (1996, RFC2030). Versioune 1-4 si matenee kompatibel, si ënnerscheede sech nëmmen an den Algorithmen vun de Serveren.

Packet Format

Schreiwen engem einfachen NTP Client

Leap Indikator (Korrekturindikator) ass eng Zuel déi d'Sprangzweetwarnung ugeet. Sinn:

  • 0 - keng Korrektur
  • 1 - déi lescht Minutt vum Dag enthält 61 Sekonnen
  • 2 - déi lescht Minutt vum Dag enthält 59 Sekonnen
  • 3 - Serverfehler (Zäit aus der Synchroniséierung)

Versiounsnummer (Versiounsnummer) - NTP Protokoll Versiounsnummer (1-4).

Moud (Modus) - Operatiounsmodus vum Paket Sender. Wäert vun 0 bis 7, am meeschte verbreet:

  • 3 - Client
  • 4 - Server
  • 5 - Sendung Modus

Schicht (Layer Niveau) - d'Zuel vun Zwëschen Schichten tëscht dem Server an der Referenz Auer (1 - de Server hëlt Daten direkt aus der Referenz Auer, 2 - de Server hëlt Daten aus dem Server mat Niveau 1, etc.).
Pool ass en ënnerschriwwene ganzt Zuel deen de maximalen Intervall tëscht konsekutiv Messagen representéiert. Den NTP-Client spezifizéiert hei den Intervall, mat deem et erwaart de Server ze pollen, an den NTP-Server spezifizéiert den Intervall, mat deem et erwaart ze ginn. De Wäert ass gläich mam binäre Logarithmus vu Sekonnen.
Präzisioun (Präzisioun) ass en ënnerschriwwene Ganzt, deen d'Genauegkeet vun der Systemuhr representéiert. De Wäert ass gläich mam binäre Logarithmus vu Sekonnen.
root Verzögerung (Serverlatenz) ass d'Zäit déi et brauch fir d'Auer op den NTP-Server z'erreechen, als fixe Punktzuel vu Sekonnen.
Root Dispersioun (Serverstreuung) - D'Streuung vun der NTP-Serveruhr als fixe Punktzuel vu Sekonnen.
Ref id (Quell ID) - kucken ID. Wann de Server Stratum 1 huet, dann ass ref id den Numm vun der atomarer Auer (4 ASCII Zeechen). Wann de Server en anere Server benotzt, dann enthält d'Ref ID d'Adress vun dësem Server.
Déi lescht 4 Felder representéieren d'Zäit - 32 Bits - den ganzen Deel, 32 Bits - de Fraktiounsdeel.
Referenz - déi lescht Auer um Server.
Urspronk - Zäit wou de Pak geschéckt gouf (ausgefëllt vum Server - méi doriwwer ënnen).
Kréien - Zäit wou de Paket vum Server opgeholl gouf.
weiderginn - Zäit wou de Paket vum Server op de Client geschéckt gouf (ausgefëllt vum Client, méi doriwwer hei ënnen).

Déi lescht zwee Felder ginn net berücksichtegt.

Loosst eis eise Package schreiwen:

Package Code

class NTPPacket:
    _FORMAT = "!B B b b 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

Fir e Paket op de Server ze schécken (a kréien), musse mir et an eng Array vu Bytes ëmsetzen.
Fir dës (an ëmgedréint) Operatioun schreiwen mir zwou Funktiounen - pack () an unpack ():

pack Funktioun

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 Funktioun

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

Fir faul Leit, als Applikatioun - Code deen de Package an e schéine String mécht

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)

E Pak op de Server schécken

Schéckt e Paket mat gefëllte Felder op de Server Versioun, Moud и weiderginn. d' weiderginn Dir musst déi aktuell Zäit op der lokal Maschinn uginn (Zuel vun Sekonnen zënter Januar 1, 1900), Versioun - jiddereng vun 1-4, Modus - 3 (Client Modus).

De Server, deen d'Ufro kritt huet, fëllt all Felder am NTP Paket aus, kopéiert an d'Feld Urspronk Wäert vun weiderginn, déi an der Demande koum. Et ass fir mech e Geheimnis firwat de Client net direkt de Wäert vu senger Zäit am Feld kann ausfëllen Urspronk. Als Resultat, wann de Pak zréck kënnt, huet de Client 4 Zäitwäerter - d'Zäit wou d'Ufro geschéckt gouf (Urspronk), d'Zäit wou de Server d'Ufro krut (Kréien), d'Zäit wou de Server d'Äntwert geschéckt huet (weiderginn) an d'Zäit vun der Empfang vun enger Äntwert vum Client - ukommen (net am Pak). Mat dëse Wäerter kënne mir déi richteg Zäit setzen.

Package schéckt an Empfang 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)

Datenveraarbechtung vum Server

D'Veraarbechtung vun den Donnéeën vum Server ass ähnlech wéi d'Handlunge vum engleschen Här aus dem ale Problem vum Raymond M. Smallian (1978): "Eng Persoun hat keng Armbanduhr, mä et war eng genee Maueruhr doheem, déi hien huet. heiansdo vergiess ze Wand. Enges Daags, vergiess d'Auer erëm unzefänken, ass hien op säi Frënd besichen, den Owend mat him verbruecht, a wéi hien zréckkoum, huet hien et fäerdeg bruecht d'Auer richteg ze setzen. Wéi huet hien dat fäerdeg bruecht, wann d'Reeszäit net am Viraus bekannt war? D'Äntwert ass: "D'Haus verlassen, eng Persoun wéckelt d'Auer op an erënnert sech un d'Positioun vun den Hänn. Kommt bei e Frënd an verloosst d'Gäscht, notéiert hien d'Zäit vu senger Arrivée an Depart. Dëst erlaabt him erauszefannen wéi laang hien ewech war. Heem zréck a kuckt op d'Auer, bestëmmt eng Persoun d'Dauer vu senger Verontreiung. Vun dëser Zäit subtrahéiert d'Zäit, déi hien op Besuch verbruecht huet, fënnt d'Persoun d'Zäit op der Strooss dohinner an zréck. Andeems hien d'Halschent vun der Zäit op der Strooss bäidréit fir d'Gäscht ze verloossen, kritt hien d'Méiglechkeet d'Zäit vun der Arrivée doheem erauszefannen an d'Hänn vu senger Auer deementspriechend unzepassen.

Fannt d'Zäit wou de Server un der Ufro geschafft huet:

  1. Fannt d'Paketreeszäit vum Client op de Server: ((Arrive - Originate) - (Transmit - Receive)) / 2
  2. Fannt den Ënnerscheed tëscht Client an Server Zäit:
    Receive - Originate - ((Arrive - Originate) - (Transmit - Receive)) / 2 =
    2 * Empfang - 2 * Originéieren - Arrivée + Originéieren + Transmit - Empfang =
    Empfang - Originéieren - Arrivée + Iwwerdroen

Mir addéieren de kritt Wäert op d'lokal Zäit a genéisst d'Liewen.

Resultat Ausgang

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ëtzlech ze verschécken.

Source: will.com

Setzt e Commentaire