Pisanje preprostega odjemalca NTP

Pozdravljeni, Habrausers. Danes želim govoriti o tem, kako napisati svojega preprostega odjemalca NTP. V bistvu se bo pogovor obrnil na strukturo paketa in način obdelave odgovora s strežnika NTP. Koda bo napisana v Pythonu, ker se mi zdi, da boljšega jezika za take stvari preprosto ni. Poznavalci bodo opazili podobnost kode s kodo ntplib - "navdihnila" me je.

Torej, kaj točno je NTP? NTP je protokol za interakcijo s strežniki točnega časa. Ta protokol se uporablja v številnih sodobnih napravah. Na primer storitev w32tm v sistemu Windows.

Skupaj obstaja 5 različic protokola NTP. Prva, različica 0 (1985, RFC958)), trenutno velja za zastarelo. Zdaj se uporabljajo novejši, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) in 4. (1996, RFC2030). Različice 1-4 so med seboj kompatibilne, razlikujejo se le v algoritmih delovanja strežnika.

Oblika paketa

Pisanje preprostega odjemalca NTP

Indikator preskoka (indikator popravka) - številka, ki označuje opozorilo o koordinacijski sekundi. Pomen:

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

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

način (način) — način delovanja pošiljatelja paketa. Vrednost od 0 do 7, najbolj pogosta:

  • 3 – stranka
  • 4 – strežnik
  • 5 – način oddajanja

stratum (layering level) – število vmesnih slojev med strežnikom in referenčno uro (1 – strežnik vzame podatke neposredno iz referenčne ure, 2 – strežnik vzame podatke od strežnika s plastjo 1 itd.).
Bazen je predznačeno celo število, ki predstavlja največji interval med zaporednimi sporočili. Odjemalec NTP tukaj poda interval, v katerem pričakuje, da bo vprašal strežnik, strežnik NTP pa poda interval, v katerem pričakuje, da bo vprašan. Vrednost je enaka binarnemu logaritmu sekund.
Precision (natančnost) je celo število s predznakom, ki predstavlja točnost sistemske ure. Vrednost je enaka binarnemu logaritmu sekund.
Zakasnitev korenin (zakasnitev strežnika) – čas, ki je potreben, da odčitki ure dosežejo strežnik NTP, kot število sekund s fiksno vejico.
Razpršenost korenin (server spread) - širjenje odčitkov ure strežnika NTP kot število sekund s fiksno točko.
ID ref (identifikator vira) ​​– ID ure. Če ima strežnik stratum 1, potem je ref id ime atomske ure (4 znaki ASCII). Če strežnik uporablja drug strežnik, potem ref ID vsebuje naslov tega strežnika.
Zadnja 4 polja predstavljajo čas - 32 bitov - celo število, 32 bitov - delni del.
Reference — najnovejši odčitki ure na strežniku.
Izvirajo – čas, ko je bil paket poslan (izpolni strežnik – več o tem spodaj).
Prejeti – čas, ko je strežnik prejel paket.
Transmit – čas pošiljanja paketa s strežnika odjemalcu (izpolni odjemalec, več o tem v nadaljevanju).

Zadnjih dveh polj ne bomo upoštevali.

Napišimo naš paket:

Koda paketa

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

Da pošljemo (in sprejmemo) paket na strežnik, ga moramo znati spremeniti v niz bajtov.
Za to (in obratno) operacijo bomo napisali dve funkciji - pack() in unpack():

funkcija pakiranja

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

funkcijo razpakiranja

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 lenuhe, kot aplikacija - koda, ki spremeni paket v čudovit niz

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)

Pošiljanje paketa na strežnik

Paket z izpolnjenimi polji mora biti poslan na strežnik različica, način и Transmit. V Transmit določiti morate trenutni čas na lokalnem računalniku (število sekund od 1. januarja 1900), različica - katera koli od 1-4, način - 3 (odjemalski način).

Strežnik, ko sprejme zahtevo, izpolni vsa polja v paketu NTP in kopira v polje Izvirajo vrednost od Transmit, ki je prišla v zahtevi. Zame je skrivnost, zakaj stranka ne more takoj zapolniti vrednosti svojega časa na terenu Izvirajo. Kot rezultat, ko se paket vrne, ima odjemalec 4 časovne vrednosti - čas, ko je bila zahteva poslana (Izvirajo), čas, ko je strežnik prejel zahtevo (Prejeti), čas, ko je strežnik poslal odgovor (Transmit) in čas, ko je stranka prejela odgovor – Prihod (ni v paketu). Z uporabo teh vrednosti lahko nastavimo točen čas.

Koda za pošiljanje in prejemanje 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)

Obdelava podatkov s strežnika

Obdelava podatkov s strežnika je podobna dejanjem angleškega gospoda iz starega problema Raymonda M. Smullyana (1978): »En človek ni imel ročne ure, doma pa je bila točna stenska ura, ki jo je včasih pozabil. vetriti. Nekega dne, ko je spet pozabil naviti uro, je šel na obisk k prijatelju, z njim preživel večer in ko se je vrnil domov, mu je uspelo uro pravilno nastaviti. Kako mu je to uspelo, če čas potovanja ni bil vnaprej znan? Odgovor je: »Človek ob odhodu od doma navije uro in si zapomni, v kakšnem položaju so kazalci. Ko pride k prijatelju in zapusti goste, zabeleži čas svojega prihoda in odhoda. To mu omogoča, da ugotovi, kako dolgo je bil na obisku. Ko se vrne domov in pogleda na uro, oseba določi trajanje svoje odsotnosti. Če človek od tega časa odšteje čas, ki ga je preživel na obisku, ugotovi čas, ki ga je porabil za potovanje tja in nazaj. Če k času odhoda gostov prišteje polovico časa, preživetega na poti, dobi možnost, da ugotovi uro prihoda domov in temu primerno prilagodi kazalce svoje ure.”

Poiščite čas, ko strežnik dela na zahtevo:

  1. Poiščite čas potovanja paketa od odjemalca do strežnika: ((Prihod – Izhod) – (Oddaj – Sprejmi)) / 2
  2. Poiščite razliko med časom odjemalca in strežnika:
    Prejemanje - Izhajanje - ((Prihod - Izhajanje) - (Pošiljanje - Sprejemanje)) / 2 =
    2 * Prejemanje – 2 * Izvor – Prihod + Izvor + Pošiljanje – Sprejemanje =
    Sprejmi – Odpremi – Prispej + Oddaj

Dobljeno vrednost dodamo lokalnemu času in uživamo življenje.

Izpis rezultata

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)

Uporabno povezava.

Vir: www.habr.com

Dodaj komentar