Písanie jednoduchého NTP klienta

Ahojte habrauzieri. Dnes chcem hovoriť o tom, ako napísať vlastného jednoduchého klienta NTP. V zásade sa konverzácia zmení na štruktúru paketu a na to, ako sa spracováva odpoveď zo servera NTP. Kód bude napísaný v pythone, pretože podľa mňa na takéto veci jednoducho neexistuje lepší jazyk. Fajnšmekri si dajú pozor na podobnosť kódu s kódom ntplib – tým som sa „inšpiroval“.

Takže čo je vlastne NTP? NTP je protokol na komunikáciu s časovými servermi. Tento protokol sa používa v mnohých moderných strojoch. Napríklad služba w32tm na oknách.

Celkovo existuje 5 verzií protokolu NTP. Prvá verzia 0 (1985, RFC958) sa v súčasnosti považuje za zastaranú. V súčasnosti sa používajú novšie, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) a 4. (1996, RFC2030). Verzie 1-4 sú navzájom kompatibilné, líšia sa len v algoritmoch serverov.

Formát paketu

Písanie jednoduchého NTP klienta

Indikátor skoku (indikátor korekcie) je číslo, ktoré označuje upozornenie na skokovú sekundu. Význam:

  • 0 - žiadna korekcia
  • 1 - posledná minúta dňa obsahuje 61 sekúnd
  • 2 - posledná minúta dňa obsahuje 59 sekúnd
  • 3 - zlyhanie servera (nesynchronizovaný čas)

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

režim (mode) — prevádzkový režim odosielateľa paketov. Hodnota od 0 do 7, najčastejšie:

  • 3 - klient
  • 4 - server
  • 5 - režim vysielania

vrstva (úroveň vrstvenia) - počet medzivrstiev medzi serverom a referenčnými hodinami (1 - server berie údaje priamo z referenčných hodín, 2 - server berie údaje zo servera s úrovňou 1 atď.).
bazén je celé číslo so znamienkom predstavujúce maximálny interval medzi po sebe nasledujúcimi správami. Klient NTP tu špecifikuje interval, v ktorom očakáva, že bude server vyzvaný, a server NTP špecifikuje interval, v ktorom očakáva, že bude vyzvaný. Hodnota sa rovná binárnemu logaritmu sekúnd.
Presnosť (presnosť) je celé číslo so znamienkom predstavujúce presnosť systémových hodín. Hodnota sa rovná binárnemu logaritmu sekúnd.
oneskorenie koreňov (latencia servera) je čas, za ktorý sa hodiny dostanú na server NTP, vyjadrený v sekundách s pevnou bodkou.
koreňová disperzia (rozptyl servera) – Rozptyl hodín servera NTP ako počet sekúnd s pevným bodom.
Ref id (ID zdroja) – ID sledovania. Ak má server vrstvu 1, potom ref id je názov atómových hodín (4 znaky ASCII). Ak server používa iný server, potom ref id obsahuje adresu tohto servera.
Posledné 4 polia predstavujú čas - 32 bitov - celočíselná časť, 32 bitov - zlomková časť.
Referencie - najnovšie hodiny na serveri.
Pochádzajú – čas, kedy bol paket odoslaný (vyplnený serverom – viac nižšie).
obdržať – čas, kedy bol paket prijatý serverom.
Odovzdať – čas, kedy bol paket odoslaný zo servera klientovi (vyplní klient, viac nižšie).

Posledné dve polia sa nebudú brať do úvahy.

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

Aby sme mohli poslať (a prijať) paket na server, musíme byť schopní ho premeniť na pole bajtov.
Pre túto (a spätnú) operáciu napíšeme dve funkcie - pack() a unpack():

funkcia balenia

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 rozbalenia

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 lenivcov ako aplikácia - kód, ktorý premení balíček na krásnu šnúrku

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

Odošlite paket s vyplnenými poľami na server verzia, režim и Odovzdať. V Odovzdať musíte zadať aktuálny čas na lokálnom počítači (počet sekúnd od 1. januára 1900), verzia - ľubovoľná z 1-4, režim - 3 (režim klienta).

Server po prijatí požiadavky vyplní všetky polia v pakete NTP a skopíruje ich do poľa Pochádzajú hodnota od Odovzdať, ktorý prišiel v žiadosti. Je mi záhadou, prečo klient nevie hneď vyplniť hodnotu svojho času v teréne Pochádzajú. Výsledkom je, že keď sa paket vráti, klient má 4 časové hodnoty - čas odoslania požiadavky (Pochádzajú), čas, keď server prijal požiadavku (obdržať), čas, keď server odoslal odpoveď (Odovzdať) a čas prijatia odpovede klientom - doraziť (nie je súčasťou balenia). Pomocou týchto hodnôt môžeme nastaviť správny čas.

Kód odosielania a prijímania 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 údajov zo servera je podobné počínaniu anglického gentlemana zo starého problému Raymonda M. Smalliana (1978): „Jeden človek nemal náramkové hodinky, ale doma boli presné nástenné hodiny, ktoré niekedy zabudol navetrať. Jedného dňa, keď zabudol znova spustiť hodiny, išiel navštíviť svojho priateľa, strávil s ním večer, a keď sa vrátil domov, podarilo sa mu správne nastaviť hodiny. Ako sa mu to podarilo, ak čas cesty nebol vopred známy? Odpoveď znie: „Pri odchode z domu si človek natiahne hodiny a zapamätá si polohu ručičiek. Pri príchode k priateľovi a odchode od hostí si všimne čas svojho príchodu a odchodu. To mu umožňuje zistiť, ako dlho bol preč. Po návrate domov a pri pohľade na hodiny človek určuje trvanie svojej neprítomnosti. Odpočítaním od tohto času času, ktorý strávil na návšteve, človek zistí čas strávený na ceste tam a späť. Pripočítaním polovice času stráveného na ceste k času odchodu hostí získa možnosť zistiť čas príchodu domov a prispôsobiť si ručičky svojich hodín.

Nájdite čas, kedy server pracoval na požiadavke:

  1. Vyhľadanie času cestovania paketu z klienta na server: ((Príchod - Pôvod) - (Odoslať - Prijať)) / 2
  2. Nájdite rozdiel medzi časom klienta a servera:
    Prijať – Pôvod – ((Príchod – Pôvod) – (Odoslať – Prijať)) / 2 =
    2 * Príjem - 2 * Pôvod - Príchod + Pôvod + Odoslanie - Príjem =
    Prijať - Pôvod - Príchod + Odoslanie

Pridanú hodnotu pridávame k miestnemu času a užívame si život.

Výsledok Výstup

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)

Užitočné odkaz.

Zdroj: hab.com

Pridať komentár