Pisanje jednostavnog NTP klijenta

Zdravo, Habrausers. Danas želim govoriti o tome kako napisati svoj jednostavan NTP klijent. U osnovi, razgovor će se okrenuti strukturi paketa i načinu obrade odgovora sa NTP servera. Kod će biti napisan na Pythonu, jer mi se čini da jednostavno nema boljeg jezika za takve stvari. Poznavaoci će primijetiti sličnost koda sa ntplib kodom - bio sam "inspiriran" njime.

Dakle, šta je zapravo NTP? NTP je protokol za interakciju sa serverima tačnog vremena. Ovaj protokol se koristi u mnogim modernim mašinama. Na primjer, usluga w32tm u Windowsima.

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 servera.

Format paketa

Pisanje jednostavnog NTP klijenta

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

  • 0 – nema 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 (režim) — režim rada pošiljaoca paketa. Vrijednost od 0 do 7, najčešće:

  • 3 – klijent
  • 4 – server
  • 5 – način emitovanja

stratum (nivo slojeva) – broj međuslojeva između servera i referentnog sata (1 – server uzima podatke direktno iz referentnog sata, 2 – server preuzima podatke sa servera sa slojem 1, itd.).
bazen je predpisani cijeli broj koji predstavlja maksimalni interval između uzastopnih poruka. NTP klijent ovdje specificira interval u kojem očekuje da će anketirati server, a NTP server specificira interval u kojem očekuje da će biti ispitan. Vrijednost je jednaka binarnom logaritmu sekundi.
preciznost (preciznost) je predznačeni cijeli broj koji predstavlja tačnost sistemskog sata. Vrijednost je jednaka binarnom logaritmu sekundi.
Root kašnjenje (kašnjenje servera) – vrijeme potrebno da očitanja sata stignu do NTP servera, kao broj sekundi s fiksnom tačkom.
Disperzija korijena (server spread) - širenje očitavanja sata NTP servera kao broj sekundi sa fiksnom tačkom.
Ref id (izvorni identifikator) – id sata. Ako server ima stratum 1, tada je ref id naziv atomskog sata (4 ASCII znaka). Ako server koristi drugi server, tada ref id sadrži adresu ovog servera.
Posljednja 4 polja predstavljaju vrijeme - 32 bita - cijeli broj, 32 bita - razlomak.
upućivanje — najnovija očitavanja sata na serveru.
Poticati – vrijeme kada je paket poslan (popunjava server - više o tome u nastavku).
dobiti – vrijeme kada je server primio paket.
prenijeti – vrijeme slanja paketa sa servera na klijenta (popunjava klijent, više o tome u nastavku).

Posljednja 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 server, moramo biti u mogućnosti da ga pretvorimo u niz bajtova.
Za ovu (i obrnuto) operaciju napisaćemo dvije funkcije - pack() i unpack():

funkcija pakovanja

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 raspakivanja

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 ljude, kao aplikacija - kod koji pretvara paket 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 server

Paket sa popunjenim poljima mora biti poslat na server verzija, način и prenijeti. The prenijeti morate navesti trenutno vrijeme na lokalnom stroju (broj sekundi od 1. januara 1900.), verziju - bilo koja od 1-4, mod - 3 (klijentski način).

Server, nakon što je prihvatio zahtjev, popunjava sva polja u NTP paketu, kopirajući u polje Poticati vrijednost od prenijeti, koji je došao u zahtjevu. Za mene je misterija zašto klijent ne može odmah da unese vrednost svog vremena na terenu Poticati. Kao rezultat toga, kada se paket vrati, klijent ima 4 vremenske vrijednosti - vrijeme kada je zahtjev poslan (Poticati), vrijeme kada je server primio zahtjev (dobiti), vrijeme kada je server poslao odgovor (prenijeti) i vrijeme kada je klijent primio odgovor – Dođite (nije u pakovanju). Koristeći ove vrijednosti možemo postaviti tačno vrijeme.

Šifra za slanje i prijem 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 sa servera

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 tačan zidni sat, koji je ponekad zaboravljao to wind. Jednog dana, zaboravivši da ponovo namota sat, otišao je u posetu svom prijatelju, proveo veče sa njim, a kada se vratio kući uspeo je da pravilno podesi sat. Kako je to uspio ako vrijeme putovanja nije bilo unaprijed poznato? Odgovor je: „Na izlasku iz kuće čovek namotava sat i pamti u kom su položaju kazaljke. Došavši kod prijatelja i napustivši goste, bilježi vrijeme njegovog dolaska i odlaska. To mu omogućava da sazna koliko dugo je bio u posjeti. Vraćajući se kući i gledajući na sat, osoba određuje trajanje svog odsustva. Oduzimajući od ovog vremena vrijeme koje je proveo u posjeti, osoba saznaje vrijeme provedeno na putovanju tamo i nazad. Dodavanjem polovine vremena provedenog na putu vremenu odlaska gostiju dobija priliku da sazna vrijeme dolaska kući i prema tome podesi kazaljke na svom satu.”

Pronađite vrijeme kada server radi na zahtjevu:

  1. Pronađite vrijeme putovanja paketa od klijenta do servera: ((Dolazak – Izvor) – (Slanje – Prijem)) / 2
  2. Pronađite razliku između vremena klijenta i servera:
    Primi - Pokreni - ((Dolazak - Porijeklo) - (Predaj - Prijem)) / 2 =
    2 * Prijem – 2 * Porijeklo – Dolazak + Porijeklo + Prijenos – Prijem =
    Prijem – Porijeklo – 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)

Korisno link.

izvor: www.habr.com

Dodajte komentar