Pišemo jednostavan NTP klijent

Pozdrav svima! Danas ću pričati o tome kako napisati jednostavan NTP klijent. Uglavnom, razgovor će se fokusirati na strukturu paketa i način obrade odgovora s NTP servera. Kod će biti napisan u Pythonu, jer smatram da ne postoji bolji jezik za ovakve stvari. Poznavatelji će primijetiti sličnost koda s kodom ntplib — upravo sam se njime 'inspirirao'.

Dakle, što je uopće NTP? NTP je protokol za interakciju s poslužiteljima točnog vremena. Ovaj protokol se koristi u mnogim modernim strojevima. Na primjer, usluga w32tm u Windowsu.

Postoji ukupno 5 verzija NTP protokola. Prva, 0. verzija (1985, RFC958), trenutno se smatra zastarjelom. Sada se koriste novije verzije, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) i 4. (1996, RFC2030). Verzije 1-4 su međusobno kompatibilne, razlikuju se samo po algoritmima rada. poslužitelja.

Format paketa

Pišemo jednostavan NTP klijent

Leap indicator (indikator korekcije) — broj koji pokazuje upozorenje o sekundi koordinatora. Vrijednost:

  • 0 – nema korekcije
  • 1 – posljednja minuta dana sadrži 61 sekundu
  • 2 – posljednja minuta dana sadrži 59 sekundi
  • 3 – kvar na serveru (vrijeme nije sinkronizirano)

Version number (broj verzije) – broj verzije NTP protokola (1-4).

Mode (mod) — način rada pošiljatelja paketa. Vrijednost od 0 do 7, najčešće:

  • 3 – klijent
  • 4 – server
  • 5 – širokofrekventni način

Stratum (razina slojevanja) – broj međuslojeva između servera i referentnih sati (1 – server dobiva podatke izravno s referentnih sati, 2 – server dobiva podatke s servera razine 1 itd.).
Pool — cijeli broj sa znakom, koji predstavlja maksimalni interval između uzastopnih poruka. NTP klijent ovdje naznačuje interval s kojim planira anketirati server, a NTP server - interval s kojim planira biti anketiran. Vrijednost je jednaka binarnom logaritmu sekundi.
Precision (preciznost) — cijeli broj sa znakom, koji predstavlja preciznost sistemskih sati. Vrijednost je jednaka binarnom logaritmu sekundi.
Root delay (kašnjenje servera) – vrijeme koje je potrebno da podaci dođu do NTP servera, izraženo kao broj sekundi s fiksnom točkom.
Root dispersion (raspršenost očitanja servera) — raspodjela očitanja NTP servera izražena kao broj sekundi s fiksnom točkom.
Ref id (identifikator izvora) – id sata. Ako server ima stratum 1, ref id je naziv atomskih satova (4 ASCII znaka). Ako server koristi drugi server, adresu tog servera zapisuje ref id.
Posljednja 4 polja predstavljaju vrijeme – 32 bita – cijeli dio, 32 bita – decimalni dio.
Referenca — posljednje očitanje sata na serveru.
Originate – vrijeme kada je paket poslan (popunjava ga server – o tome u nastavku).
Receive – vrijeme prijema paketa od strane servera.
Transmit – vrijeme slanja paketa s servera klijentu (popunjava ga klijent, o tome također u nastavku).

Dva posljednja polja nećemo razmatrati.

Napisat ćemo naš paket:

Kod paketa

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

    def __init__(self, version_number=2, mode=3, transmit=0):
        # Potrebno je unijeti prijelazni sekund (2 bita)
        self.leap_indicator = 0
        # Verzija protokola (3 bita)
        self.version_number = version_number
        # Način pošiljatelja (3 bita)
        self.mode = mode
        # Razina "slojevanja" očitanja vremena (1 bajt)
        self.stratum = 0
        # Interval između zahtjeva (1 bajt)
        self.pool = 0
        # Preciznost (log2) (1 bajt)
        self.precision = 0
        # Interval za pristup NTP serveru (4 bajta)
        self.root_delay = 0
        # Rashod sata NTP-servera (4 bajta)
        self.root_dispersion = 0
        # Indikator satova (4 bajta)
        self.ref_id = 0
        # Vrijeme posljednje izmjene na serveru (8 bajtova)
        self.reference = 0
        # Vrijeme slanja paketa s lokalnog računala (8 bajtova)
        self.originate = 0
        # Vrijeme prijema na serveru (8 bajtova)
        self.receive = 0
        # Vrijeme slanja odgovora s servera (8 bajtova)
        self.transmit = transmit

Da bismo slali (i primali) paket na server, moramo znati pretvoriti ga u niz bajtova.
Za ovu (i obrnutu) operaciju napisat ćemo dvije funkcije — pack() i unpack():

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

Funkcija 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

Za lijene, u obliku dodatka – kod koji paketi pretvara u lijepu string

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)

Slanje paketa na server

Na server treba poslati paket s ispunjenim poljima Verzija, Mode i Transmit. U Transmit potrebno je navesti trenutno vrijeme na lokalnom računalu (broj sekundi od 1. siječnja 1900.), verzija – bilo koja od 1-4, način – 3 (klijentski način).

Server, nakon prihvaćanja zahtjeva, ispunjava sve podatke u NTP paketu kopiranjem u polje Originate vrijednosti iz Transmit, zaprimljenog u zahtjevu. Čudno mi je zašto klijent ne može odmah ispuniti vrijednost svog vremena u polju Originate. Na kraju, kada paket dođe natrag, klijent ima 4 vrijednosti vremena – vrijeme slanja zahtjeva (Originate), vrijeme prijema zahtjeva od strane servera (Receive), vrijeme slanja odgovora od servera (Transmit) i vrijeme prijema odgovora od klijenta – Stići (nije u paketu). Uz pomoć ovih vrijednosti možemo utvrditi točno vrijeme.

Kod za slanje i primanje paketa

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

Obrada podataka s servera

Obrada podataka s poslužitelja nalikuje postupcima engleskog džentlmena iz starog zadatka Raymonda M. Smullyana (1978): „Jednoj osobi nije bilo dostupno ručno vrijeme, ali su kod kuće visili točni zidni satovi koje je povremeno zaboravljao namjestiti. Jednom prilikom, zaboravivši ponovno nastaviti sat, otišao je u posjet svom prijatelju, proveo s njim večer, a kada se vratio kući, uspješno je namjestio sat. Kako mu je to uspjelo, ako vrijeme putovanja nije bilo unaprijed poznato?“ Odgovor je sljedeći: „Izlazeći iz kuće, osoba namješta sat i pamti u kojem su položaju kazaljke. Kada dođe kod prijatelja i odlazi s posjeta, bilježi vrijeme svog dolaska i odlaska. To mu omogućuje da sazna koliko je vremena proveo kod prijatelja. Vraćajući se kući i pogledajući na sat, osoba određuje trajanje svog odsustva. Oduzimajući od tog vremena vrijeme koje je provela kod prijatelja, saznaje vrijeme potrošeno za putovanje ondje i natrag. Dodajući polovinu vremena potrošenog na putovanje tome vremenu kada je napustila prijatelja, dobiva mogućnost da sazna vrijeme svog dolaska kući i pravilno premjesti kazaljke svojih satova.

Određujemo vrijeme obrade poslužitelja za zahtjev:

  1. Određujemo vrijeme putovanja paketa od klijenta do poslužitelja: ((Dolazak – Izvor) — (Prijenos – Primanje)) ÷ 2
  2. Određujemo razliku između vremena klijenta i poslužitelja:
    Primanje — Izvor — ((Dolazak – Izvor) — (Prijenos – Primanje)) ÷ 2 =
    2 * Primanje — 2 * Izvor – Dolazak + Izvor + Prijenos – Primanje =
    Primanje – Izvor – Dolazak + Prijenos

Dodajemo dobivenu vrijednost lokalnom vremenu i radujemo se životu.

Ispis rezultata

time_different = answer.get_time_different(arrive_time)
result = "Razlika u vremenu: {}nVrijeme poslužitelja: {}n{}".format(
    time_different,
    datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
    answer.to_display())
print(result)

Koristan link.

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster