Pisanje preprostega NTP odjemalca

Pozdravljeni, uporabniki Habr. Danes bi rad spregovoril o tem, kako napisati svoj preprost NTP odjemalec. Osredotočal se bom na strukturo paketa in način obdelave odgovora z NTP strežnika. Koda bo napisana v Pythonu, saj mislim, da boljšega jezika za take stvari preprosto ni. Poznavalci bodo opazili podobnost kode s kodo ntplib — ravno ta me je 'navdihnila'.

Torej, kaj pravzaprav je NTP? NTP je protokol za interakcijo s strežniki natančnega časa. Ta protokol se uporablja v mnogih sodobnih napravah. Na primer, storitev w32tm v Windows.

Obstaja pet različic protokola NTP. Prva, ničta različica (1985, RFC958), je trenutno zastarela. Trenutno se uporabljajo novejše, prva (1988, RFC1059), druga (1989, RFC1119), tretja (1992, RFC1305) in četrta (1996, RFC2030). Različice 1-4 so medsebojno združljive, razlikujejo se le v algoritmih delovanja. strežnikov.

Format paketa

Pisanje preprostega NTP odjemalca

Leap indicator (indikator korekcije) — številka, ki opozarja na sekundo korekcije. Vrednost:

  • 0 – brez korekcije
  • 1 – zadnja minuta dneva ima 61 sekund
  • 2 – zadnja minuta dneva ima 59 sekund
  • 3 – okvara strežnika (čas ni sinhroniziran)

Version number (številka različice) – številka različice protokola NTP (1-4).

Mode (način) — način delovanja pošiljatelja paketa. Vrednosti od 0 do 7, najpogostejše:

  • 3 – odjemalec
  • 4 – strežnik
  • 5 – širokopasovni način

Stratum (stopnja plasti) – število vmesnih plasti med strežnikom in referenčnimi urami (1 – strežnik pridobi podatke neposredno iz referenčnih ur, 2 – strežnik pridobi podatke s strežnika prve stopnje itd.).
Pool — celo število s predznakom, ki predstavlja največji interval med zaporednimi sporočili. NTP odjemalec tukaj navaja interval, s katerim predvideva, da bo poizvedoval po strežniku, NTP strežnik pa interval, s katerim predvideva, da ga bodo poizvedovali. Vrednost je enaka dvojnemu logaritmu sekund.
Precision (natančnost) — celo število s predznakom, ki predstavlja natančnost sistemskih ur. Vrednost je enaka dvojnemu logaritmu sekund.
Root delay (zamuda strežnika) – čas, ki ga potrebujejo podatki ur, da prispejo do NTP strežnika, kot število sekund s fiksno decimalno točko.
Root dispersion (razpršenost meritev strežnika) — razpršenost meritev ur NTP strežnika kot število sekund s fiksno decimalno točko.
Ref id (identifikator izvora) – id sati. Ako server ima stratum 1, tada ref id – naziv atomskih sati (4 ASCII znakova). Ako server koristi drugi server, tada se ref id upisuje adresa tog servera.
Posljednja 4 polja predstavljaju vrijeme – 32 bita – cijeli dio, 32 bita – decimalni dio.
Referenca — posljednja očitanja sati na serveru.
Originate – vrijeme kada je paket poslan (ispunjava server – o tome u nastavku).
Receive – vrijeme prijema paketa od strane servera.
Transmit – vrijeme slanja paketa sa servera klijentu (ispunjava klijent, o tome također u nastavku).

Dva posljednja polja nećemo razmatrati.

Napišimo naš paket:

Kod paketa

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

    def __init__(self, version_number=2, mode=3, transmit=0):
        # Potrebno za unos skoka sekunde (2 bita)
        self.leap_indicator = 0
        # Verzija protokola (3 bita)
        self.version_number = version_number
        # Način pošiljaoca (3 bita)
        self.mode = mode
        # Nivo "slojevanja" 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 sat-u NTP servera (4 bajta)
        self.root_delay = 0
        # Raspršenje sata NTP-servera (4 bajta)
        self.root_dispersion = 0
        # Indikator sati (4 bajta)
        self.ref_id = 0
        # Vrijeme posljednje ažuriranja na serveru (8 bajtova)
        self.reference = 0
        # Vrijeme slanja paketa sa lokalne mašine (8 bajtova)
        self.originate = 0
        # Vrijeme prijema na serveru (8 bajtova)
        self.receive = 0
        # Vrijeme slanja odgovora sa 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

Для ленивых, в качестве приложения – код, превращающий пакет в красивую строку

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)

Отправка пакета на сервер

На сервер необходимо отправить пакет с заполненными полями Različica, Mode in Transmit. U Transmit необходимо указать текущее время на локальной машине (количество секунд с 1 января 1900 г), версия — любая из 1-4, режим — 3 (режим клиента).

Сервер, приняв запрос, заполняет в NTP-пакете все поля, скопировав в поле Originate значение из Transmit, пришедшее в запросе. Для меня остается загадкой, почему клиент не может сразу заполнить значение своего времени в поле Originate. В итоге, когда пакет приходит обратно, у клиента есть 4 значения времени – время отправки запроса (Originate), время получения запроса сервером (Receive), время отправки ответа сервером (Transmit) и время получения ответа клиентом – Прибытие (нет в пакете). С помощью этих значений мы можем установить корректное время.

Код отправки и получения пакета

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

Обработка данных с сервера

Obdelava podatkov s strežnika je podobna dejanju angleškega džentelmena iz stare naloge Raymonda M. Smalleyana (1978): „Enega človeka ni bilo ročnih ur, a doma so viseli natančni stenski uri, ki jih je občasno pozabljal naviti. Nekega dne, ko je spet pozabil naviti ure, se je odpravil na obisk k prijatelju, preživel večer pri njem, potem pa se vrnil domov in lahko pravilno nastavil ure. Kako mu je to uspelo, če čas potovanja vnaprej ni bil znan?“ Odgovor je tak: „Ko je odhajal od doma, je človek navil ure in si zapomnil, v katerem položaju so kazalci. Ko je prišel k prijatelju in odhajal od tam, si je zabeležil čas svojega prihoda in odhoda. To mu omogoča, da izve, koliko časa je preživel na obisku. Ko se je vrnil domov in pogledal na ure, je določil trajanje svoje odsotnosti. Tako da od tega časa odšteje tisto, kar je preživel na obisku, izve čas, porabljen za pot tja in nazaj. K časovnemu odhodu iz obiska doda polovico časa, porabljenega za pot, in tako dobi čas prihoda domov ter ustrezno prilagodi kazalce svojih ur."

Določimo čas dela strežnika na zahtevku:

  1. Določimo čas poti paketa od odjemalca do strežnika: ((Prispeti – Izvor) — (Pošlji – Prejmi)) / 2
  2. Določimo razliko med časom odjemalca in strežnika:
    Prejmi — Izvor — ((Prispeti – Izvor) — (Pošlji – Prejmi)) / 2 =
    2 * Prejmi — 2 * Izvor – Prispeti + Izvor + Pošlji – Prejmi =
    Prejmi – Izvor – Prispeti + Pošlji

Dodamo pridobljeno vrednost lokalnemu času in se veselimo življenja.

Izpis rezultata

time_different = answer.get_time_different(arrive_time)
result = "Časovna razlika: {}nČas strežnika: {}n{}".format(
    time_different,
    datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
    answer.to_display())
print(result)

Koristno povezava.

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster