Pisanje jednostavnog NTP klijenta

Pozdrav, Habrausers. Danas želim razgovarati o tome kako napisati vlastiti jednostavan NTP klijent. Uglavnom, razgovor će se okrenuti strukturi paketa i načinu obrade odgovora s NTP poslužitelja. Kod će biti napisan u Pythonu, jer mi se čini da jednostavno nema boljeg jezika za takve stvari. Poznavatelji će primijetiti sličnost koda s ntplib kodom - bio sam "inspiriran" njime.

Što je zapravo NTP? NTP je protokol za interakciju s poslužiteljima točnog vremena. Ovaj se protokol koristi u mnogim modernim strojevima. Na primjer, usluga w32tm u sustavu Windows.

Ukupno postoji 5 verzija NTP protokola. Prva, verzija 0 (1985, RFC958)), trenutno se smatra zastarjelom. Sada se koriste noviji, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) i 4. (1996, RFC2030). Verzije 1-4 su kompatibilne jedna s drugom, razlikuju se samo u algoritmima rada poslužitelja.

Format paketa

Pisanje jednostavnog NTP klijenta

Indikator skoka (indikator korekcije) - broj koji označava upozorenje o sekundi koordinacije. Značenje:

  • 0 – bez korekcije
  • 1 – zadnja minuta dana sadrži 61 sekundu
  • 2 – zadnja minuta dana sadrži 59 sekundi
  • 3 – kvar servera (vrijeme nije sinkronizirano)

Broj verzije (broj verzije) – broj verzije NTP protokola (1-4).

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

  • 3 – klijent
  • 4 – poslužitelj
  • 5 – način emitiranja

stratum (razina slojevitosti) – broj međuslojeva između poslužitelja i referentnog sata (1 – poslužitelj preuzima podatke izravno od referentnog sata, 2 – poslužitelj preuzima podatke od poslužitelja sa slojem 1 itd.).
Bazen je cijeli broj s predznakom koji predstavlja maksimalni interval između uzastopnih poruka. NTP klijent ovdje navodi interval u kojem očekuje anketu poslužitelja, a NTP poslužitelj specificira interval u kojem očekuje anketu. Vrijednost je jednaka binarnom logaritmu sekundi.
Preciznost (točnost) je cijeli broj s predznakom koji predstavlja točnost sistemskog sata. Vrijednost je jednaka binarnom logaritmu sekundi.
Odgoda korijena (kašnjenje poslužitelja) – vrijeme koje je potrebno da očitanja sata stignu do NTP poslužitelja, kao broj sekundi s fiksnom točkom.
Raspršenost korijena (server spread) - širenje očitanja sata NTP poslužitelja kao broj sekundi s fiksnom točkom.
ID ref (identifikator izvora) ​​– ID sata. Ako poslužitelj ima stratum 1, tada je ref id naziv atomskog sata (4 ASCII znaka). Ako poslužitelj koristi drugi poslužitelj, tada ref id sadrži adresu ovog poslužitelja.
Posljednja 4 polja predstavljaju vrijeme - 32 bita - cijeli broj, 32 bita - razlomak.
Upućivanje — najnovija očitanja sata na poslužitelju.
Porijeklom – vrijeme kada je paket poslan (popunjava poslužitelj – više o tome u nastavku).
Primati – vrijeme kada je poslužitelj primio paket.
Prenijeti – vrijeme slanja paketa od poslužitelja do klijenta (popunjava klijent, više o tome u nastavku).

Zadnja dva polja nećemo razmatrati.

Napišimo naš paket:

Šifra 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 bismo poslali (i primili) paket na poslužitelj, moramo ga moći pretvoriti u niz bajtova.
Za ovu (i obrnutu) operaciju, napisat ćemo dvije funkcije - pack() i 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))

funkcija raspakiranja

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 lijenčine, kao aplikacija - kod koji paket pretvara u prekrasan 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)

Slanje paketa na poslužitelj

Paket s popunjenim poljima mora biti poslan na poslužitelj Verzija, način и Prenijeti. U Prenijeti morate 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).

Poslužitelj, nakon što prihvati zahtjev, ispunjava sva polja u NTP paketu, kopirajući u polje Porijeklom vrijednost iz Prenijeti, koji je stigao u zahtjevu. Zagonetno mi je zašto klijent ne može odmah ispuniti vrijednost svog vremena na terenu Porijeklom. Kao rezultat toga, kada se paket vrati, klijent ima 4 vremenske vrijednosti - vrijeme kada je zahtjev poslan (Porijeklom), vrijeme kada je poslužitelj primio zahtjev (Primati), vrijeme kada je poslužitelj poslao odgovor (Prenijeti) i vrijeme kada je klijent primio odgovor – Prijava (nije u paketu). Pomoću ovih vrijednosti možemo postaviti točno vrijeme.

Šifra 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 poslužitelja

Obrada podataka sa servera slična je postupcima engleskog gospodina iz starog problema Raymonda M. Smullyana (1978.): „Jedan čovjek nije imao ručni sat, ali je kod kuće bio točan zidni sat koji je ponekad zaboravljao navijati. Jednog dana, zaboravivši ponovno naviti sat, otišao je posjetiti prijatelja, proveo večer s njim, a kada se vratio kući, uspio je ispravno namjestiti sat. Kako mu je to pošlo za rukom ako vrijeme putovanja nije bilo unaprijed poznato? Odgovor je: “Izlazeći od kuće, čovjek navija sat i pamti u kojem su položaju kazaljke. Došavši prijatelju i ostavivši goste, bilježi vrijeme njegova dolaska i odlaska. To mu omogućuje da sazna koliko dugo je bio u posjetu. Vraćajući se kući i gledajući na sat, osoba određuje trajanje svoje odsutnosti. Oduzimajući od ovog vremena vrijeme koje je proveo u posjeti, osoba saznaje vrijeme provedeno na putu tamo i natrag. Dodavanjem polovice vremena provedenog na putu vremenu odlaska gostiju, dobiva priliku saznati vrijeme dolaska kući i prema tome namjestiti kazaljke na svom satu.”

Pronađite vrijeme kada poslužitelj radi na zahtjevu:

  1. Pronađite vrijeme putovanja paketa od klijenta do poslužitelja: ((Dolazak – Polazak) – (Odašiljanje – Primanje)) / 2
  2. Pronađite razliku između vremena klijenta i poslužitelja:
    Prijem - Izvor - ((Dolazak - Izvor) - (Odašiljanje - Prijem)) / 2 =
    2 * Prijem – 2 * Izvor – Dolazak + Izvor + Prijem – Prijem =
    Primanje – Poreklo – Dolazak + Prijenos

Dobivenu vrijednost dodajemo lokalnom vremenu i uživamo u životu.

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

Koristan link.

Izvor: www.habr.com

Dodajte komentar