Píšeme jednoduchého NTP klienta

Ahojte, používatelia Habr. Dnes chcem hovoriť o tom, ako napísať svoj jednoduchý NTP klient. Hlavne sa budeme zaoberať štruktúrou paketu a spôsobom spracovania odpovede z NTP servera. Kód bude napísaný v Pythone, pretože, ako sa domnievam, lepší jazyk na takéto veci jednoducho nenájdete. Odborníci si určite všimnú podobnosť kódu s kódom ntplib — inšpiroval som sa práve ním.

Takže, čo vlastne NTP je? NTP – protokol interakcie so servermi presného času. Tento protokol sa používa v mnohých moderných zariadeniach. Napríklad, služba w32tm vo Windows.

Existuje celkovo 5 verzií NTP protokolu. Prvá, 0. verzia (1985, RFC958), sa v súčasnosti považuje za zastaranú. Teraz sa používajú novšie verzie: 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) a 4. (1996, RFC2030). Verzie 1-4 sú medzi sebou kompatibilné, líšia sa len pracovnými algoritmami. serverov.

Formát paketu

Píšeme jednoduchého NTP klienta

Indikátor skoku (indikátor korekcie) — číslo, ktoré ukazuje varovanie o sekunde koordinácie. Hodnota:

  • 0 – žiadna korekcia
  • 1 – posledná minúta dňa obsahuje 61 sekúnd
  • 2 – posledná minúta dňa obsahuje 59 sekúnd
  • 3 – porucha servera (čas nie je synchronizovaný)

Číslo verzie (číslo verzie) – číslo verzie protokolu NTP (1-4).

Režim (režim) — režim činnosti odosielateľa paketu. Hodnota od 0 do 7, najčastejšie:

  • 3 – klient
  • 4 – server
  • 5 – širokopásmový režim

Stratum (úroveň vrstvenia) – počet medzičlánkov medzi serverom a referenčnými hodinami (1 – server získava údaje priamo z referenčných hodín, 2 – server získava údaje zo servera s úrovňou 1 atď.).
Pool – celé číslo so znamienkom, predstavujúce maximálny interval medzi po sebe idúcimi správami. NTP klient tu uvádza interval, s ktorým predpokladá, že bude server dotazovať, a NTP server – interval, s ktorým predpokladá, že ho budú dotazovať. Hodnota je rovná dvojkovému logaritmu sekúnd.
Presnosť (presnosť) — celé číslo so znamienkom, predstavujúce presnosť systémových hodín. Hodnota je rovná dvojkovému logaritmu sekúnd.
Zdržanie koreňa (zdržanie servera) – čas, za ktorý sa merania hodín dostanú na NTP server, v sekundách s pevnou desatinnou čiarkou.
Rozptyl koreňa (rozptyl merania servera) — rozptyl merania hodín NTP servera ako číslo sekúnd s pevnou desatinnou čiarkou.
ID referencie (identifikátor zdroja) – id hodín. Ak server má stratum 1, ref id je názov atómových hodín (4 ASCII znaky). Ak server používa iný server, adresa tohto servera je v ref id.
Posledné 4 polia predstavujú čas – 32 bitov – celé číslo, 32 bitov – desatinná časť.
Referenciu – posledné merania hodín na serveri.
Pôvod – čas, kedy bol balík odoslaný (vyplňuje server – o tom nižšie).
Prijatie – čas, kedy server prijal balík.
Odoslanie – čas, kedy balík od servera poslal klient (vyplňuje klient, o tom tiež nižšie).

Dve posledné polia nebudeme zvažovať.

Napíšeme náš balík:

Kód balíka

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

    def __init__(self, version_number=2, mode=3, transmit=0):
        # Potrebné na zadanie prestupnej sekundy (2 bity)
        self.leap_indicator = 0
        # Verzia protokolu (3 bity)
        self.version_number = version_number
        # Mód odosielateľa (3 bity)
        self.mode = mode
        # Úroveň "vrstvenia" čítania času (1 byt)
        self.stratum = 0
        # Interval medzi požiadavkami (1 byt)
        self.pool = 0
        # Presnosť (log2) (1 byt)
        self.precision = 0
        # Interval na dosiahnutie NTP servera (4 bajty)
        self.root_delay = 0
        # Rozptyl na NTP serveri (4 bajty)
        self.root_dispersion = 0
        # Ukazovateľ hodín (4 bajty)
        self.ref_id = 0
        # Čas poslednej aktualizácie na serveri (8 bajtov)
        self.reference = 0
        # Čas odoslania balíka z lokálneho zariadenia (8 bajtov)
        self.originate = 0
        # Čas prijatia na serveri (8 bajtov)
        self.receive = 0
        # Čas odoslania odpovede zo servera (8 bajtov)
        self.transmit = transmit

Aby sme mohli posielať (a prijímať) balík na server, musíme vedieť prevádzať ho na pole bajtov.
Pre túto (aj spätnú) operáciu napíšeme dve funkcie – pack() a unpack():

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

Funkcia 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

Pre lenochov, ako aplikáciu – kód premení balík na pekný reťazec

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)

Odoslanie balíka na server

Na server je potrebné odoslať balík so vyplnenými poľami Verzia, Režim a Odoslanie. V Odoslanie je potrebné uviesť aktuálny čas na lokálnom počítači (počet sekúnd od 1. januára 1900), verzia – akákoľvek z 1-4, režim – 3 (režim klienta).

Server, ktorý prijal žiadosť, vyplní v NTP balíku všetky polia, skopírovaním do poľa Pôvod hodnotu z Odoslanie, prichádzajúcu v žiadosti. Pre mňa je záhadou, prečo klient nemôže okamžite vyplniť hodnotu svojho času do poľa Pôvod. Nakoniec, keď balík príde späť, má klient 4 hodnoty času – čas odoslania žiadosti (Pôvod), čas prijatia žiadosti serverom (Prijatie), čas odoslania odpovede serverom (Odoslanie) a čas prijatia odpovede klientom – Príchod (nie je v balíku). Pomocou týchto hodnôt môžeme nastaviť správny čas.

Kód na odoslanie a prijatie balíka

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

Spracovanie údajov zo servera

Spracovanie dát zo servera je podobné činom anglického gentlemana z starej úlohy Raymonda M. Smullyana (1978): „Jeden človek nemal náramkové hodinky, ale doma viseli presné stenové hodiny, ktoré občas zabúdal natiahnuť. Raz, zabudnúc natiahnuť hodiny, odišiel na návštevu k priateľovi, strávil u neho večer a po návrate domov dokázal hodiny správne nastaviť. Ako to dokázal, ak čas cesty nebol vopred známy?“ Odpoveď je taká: „Pri odchode z domu človek natiahne hodiny a zapamätá si, v akom postavení sa nachádzajú ručičky. Pri príchode k priateľovi a odchode od neho si zapíše čas svojho príchodu a odchodu. To mu umožní zistiť, koľko času strávil na návšteve. Po návrate domov a pohľade na hodiny človek určí dĺžku svojho neprítomnosti. Od tohto času odčíta čas, ktorý strávil na návšteve, a získa čas strávený na ceste tam a späť. Pridaním polovice času stráveného na ceste k času odchodu od priateľa môže zistiť čas príchodu domov a odpovedajúcim spôsobom nastaviť ručičky svojich hodín.“

Zistíme čas práce servera na požiadavke:

  1. Zistíme čas cesty paketu od klienta k serveru: ((Príchod – Pôvod) — (Prenos – Prijatie)) / 2
  2. Zistíme rozdiel medzi časom klienta a servera:
    Prijatie — Pôvod — ((Príchod – Pôvod) — (Prenos – Prijatie)) / 2 =
    2 * Prijatie — 2 * Pôvod – Príchod + Pôvod + Prenos – Prijatie =
    Prijatie – Pôvod – Príchod + Prenos

Pridáme získanú hodnotu k miestnemu času a tešíme sa zo života.

Výstup výsledku

time_different = answer.get_time_different(arrive_time)
result = "Časový rozdiel: {}nServerový čas: {}n{}".format(
    time_different,
    datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
    answer.to_display())
print(result)

Užitočné odkaz.

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster