Pisanie prostego klienta NTP.

Witajcie, użytkownicy Habr. Dziś chcę opowiedzieć o tym, jak napisać swój prosty klient NTP. W głównej mierze porozmawiamy o strukturze pakietu i sposobie przetwarzania odpowiedzi z serwera NTP. Kod zostanie napisany w Pythonie, ponieważ uważam, że nie ma lepszego języka do takich rzeczy.

Zatem, czym w ogóle jest NTP? NTP to protokół komunikacji z serwerami czasu. Protokół ten jest wykorzystywany w wielu nowoczesnych maszynach. Na przykład, usługa w32tm w systemie Windows.

Istnieje łącznie pięć wersji protokołu NTP. Pierwsza, wersja 0 (1985 r., RFC958), obecnie uznawana jest za przestarzałą. Aktualnie używane są nowsze wersje: 1 (1988, RFC1059), 2 (1989, RFC1119), 3 (1992, RFC1305) i 4 (1996, RFC2030). Wersje 1-4 są ze sobą kompatybilne, różnią się jedynie algorytmami działania. serwerów.

Format pakietu

Pisanie prostego klienta NTP.

Wskaźnik przeskoku (wskaźnik korekcji) — liczba, która pokazuje upozorowanie o sekundzie koordynacji. Wartość:

  • 0 – brak korekcji
  • 1 – ostatnia minuta dnia zawiera 61 sekund
  • 2 – ostatnia minuta dnia zawiera 59 sekund
  • 3 – uszkodzenie serwera (czas nie jest zsynchronizowany)

Numer wersji (numer wersji) – numer wersji protokołu NTP (1-4).

Tryb (tryb) — tryb pracy nadawcy pakietu. Wartości od 0 do 7, najczęstsze:

  • 3 – klient
  • 4 – serwer
  • 5 – tryb rozgłoszeniowy

Stratum (poziom niskowojenny) – liczba pośrednich warstw między serwerem a zegarami odniesienia (1 – serwer pobiera dane bezpośrednio z zegarów odniesienia, 2 – serwer pobiera dane z serwera poziomu 1 itd.).
Pool — liczba całkowita ze znakiem, reprezentująca maksymalny interwał między kolejnymi wiadomościami. Klient NTP wskazuje tutaj interwał, z jakim zamierza pytać serwer, a serwer NTP – interwał, z jakim zamierza być pytany. Wartość odpowiada logarytmowi binarnemu sekund.
Precyzja (dokładność) — liczba całkowita ze znakiem, reprezentująca dokładność zegarów systemowych. Wartość odpowiada logarytmowi binarnemu sekund.
Opóźnienie korzenia (opóźnienie serwera) – czas, w ciągu którego wskazania zegarów docierają do serwera NTP, jako liczba sekund z ustaloną częścią.
Rozproszenie korzenia (rozproszenie wskazań serwera) — rozproszenie wskazań zegarów serwera NTP jako liczba sekund z ustaloną częścią.
Id referencyjny (identyfikator źródła) – id zegara. Jeśli serwer ma stratum 1, to ref id – nazwa zegarów atomowych (4 znaki ASCII). Jeśli serwer korzysta z innego serwera, to w ref id zapisany jest adres tego serwera.
Ostatnie 4 pola przedstawiają czas – 32 bity – część całkowita, 32 bity – część ułamkowa.
Referencja — ostatnie odczyty zegara na serwerze.
Pochodzenie – czas, kiedy pakiet został wysłany (uzupełniane przez serwer – więcej informacji poniżej).
Odbiór – czas odbioru pakietu przez serwer.
Transmisja – czas wysłania pakietu z serwera do klienta (uzupełniane przez klienta, więcej informacji również poniżej).

Nie będziemy rozpatrywać dwóch ostatnich pól.

Napiszmy nasz pakiet:

Kod pakietu

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

    def __init__(self, version_number=2, mode=3, transmit=0):
        # Konieczne do wprowadzenia sekundy przestępnej (2 bity)
        self.leap_indicator = 0
        # Wersja protokołu (3 bity)
        self.version_number = version_number
        # Tryb nadawcy (3 bity)
        self.mode = mode
        # Poziom odczytu czasu (1 bajt)
        self.stratum = 0
        # Przerwa między żądaniami (1 bajt)
        self.pool = 0
        # Precyzja (log2) (1 bajt)
        self.precision = 0
        # Interwał do osiągnięcia serwera NTP (4 bajty)
        self.root_delay = 0
        # Rozproszenie zegara serwera NTP (4 bajty)
        self.root_dispersion = 0
        # Wskaźnik zegarów (4 bajty)
        self.ref_id = 0
        # Czas ostatniej aktualizacji na serwerze (8 bajtów)
        self.reference = 0
        # Czas wysłania pakietu z lokalnej maszyny (8 bajtów)
        self.originate = 0
        # Czas odbioru na serwerze (8 bajtów)
        self.receive = 0
        # Czas wysyłania odpowiedzi z serwera (8 bajtów)
        self.transmit = transmit

Aby wysłać (i odebrać) pakiet na serwer, musimy umieć przekształcić go w tablicę bajtów.
Dla tej (i odwrotnej) operacji napiszemy dwie funkcje — pack() i unpack():

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

Funkcja unpack

def unpack(self, data: bytes):
        unpacked_data = struct.unpack(NTPPacket._FORMAT, data)

        self.leap_indicator = unpacked_data[0] >> 6  # 2 bity
        self.version_number = unpacked_data[0] >> 3 & 0b111  # 3 bity
        self.mode = unpacked_data[0] & 0b111  # 3 bity

        self.stratum = unpacked_data[1]  # 1 bajt
        self.pool = unpacked_data[2]  # 1 bajt
        self.precision = unpacked_data[3]  # 1 bajt

        # 2 bajty | 2 bajty
        self.root_delay = (unpacked_data[4] >> 16) +
            (unpacked_data[4] & 0xFFFF) / 2 ** 16
         # 2 bajty | 2 bajty
        self.root_dispersion = (unpacked_data[5] >> 16) +
            (unpacked_data[5] & 0xFFFF) / 2 ** 16 

        # 4 bajty
        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 bajtów
        self.originate = unpacked_data[9] + unpacked_data[10] / 2 ** 32  # 8 bajtów
        self.receive = unpacked_data[11] + unpacked_data[12] / 2 ** 32  # 8 bajtów
        self.transmit = unpacked_data[13] + unpacked_data[14] / 2 ** 32  # 8 bajtów

        return self

Dla leniwych, jako dodatek – kod, który przekształca pakiet w ładny ciąg

def to_display(self):
        return "Wskaźnik skoku: {0.leap_indicator}n" 
                "Numer wersji: {0.version_number}n" 
                "Tryb: {0.mode}n" 
                "Stratum: {0.stratum}n" 
                "Pula: {0.pool}n" 
                "Precyzja: {0.precision}n" 
                "Opóźnienie root: {0.root_delay}n" 
                "Rozproszenie root: {0.root_dispersion}n" 
                "Id ref: {0.ref_id}n" 
                "Referencja: {0.reference}n" 
                "Pochodzenie: {0.originate}n" 
                "Odbiór: {0.receive}n" 
                "Transmisja: {0.transmit}"
                .format(self)

Wysłanie pakietu do serwera

Należy wysłać pakiet z wypełnionymi polami do serwera Wersja, Tryb i Transmisja. W Transmisja konieczne jest podanie bieżącego czasu na lokalnej maszynie (liczba sekund od 1 stycznia 1900 r.), wersja — dowolna z 1-4, tryb — 3 (tryb klienta).

Serwer, który otrzymał żądanie, uzupełnia wszystkie pola w pakiecie NTP, kopiując do pola Pochodzenie wartość z Transmisja, która przyszła w żądaniu. Dla mnie jest zagadką, dlaczego klient nie może od razu wypełnić wartości swojego czasu w polu Pochodzenie. W rezultacie, gdy pakiet wraca, klient ma 4 wartości czasu – czas wysłania żądania (Pochodzenie), czas odbioru żądania przez serwer (Odbiór), czas wysłania odpowiedzi przez serwer (Transmisja) i czas odbioru odpowiedzi przez klienta – Przybycie (nie ma w pakiecie). Dzięki tym wartościom możemy ustalić poprawny czas.

Kod wysyłania i odbierania pakietu

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

Przetwarzanie danych z serwera

Przetwarzanie danych z serwera jest analogiczne do działań angielskiego dżentelmena z zadania Raymonda M. Smaliana (1978): „Pewien człowiek nie miał zegarka, ale w domu wisiały precyzyjne zegary ścienne, które czasami zapominał nakręcać. Pewnego dnia, zapominając ponownie nakręcić zegar, udał się w odwiedziny do przyjaciela, spędził tam wieczór, a po powrocie do domu zdołał poprawnie ustawić zegar. W jaki sposób mu się to udało, skoro czas podróży nie był z góry znany?” Odpowiedź jest następująca: „Wychodząc z domu, mężczyzna nakręca zegar i zapamiętuje, w jakiej pozycji znajdują się wskazówki. Przybywając do przyjaciela i wychodząc z wizyty, zaznacza czas przybycia i wyjścia. Umożliwia mu to poznanie, jak długo przebywał u gościa. Po powrocie do domu i spojrzeniu na zegar, człowiek określa czas swojej nieobecności. Odejmując od tego czasu to, co spędził u przyjaciela, uczy się, ile czasu zajęła mu podróż tam i z powrotem. Dodając do czasu wyjścia z przyjaciela połowę czasu, jaki zajmowała podróż, uzyskuje możliwość ustalenia czasu przybycia do domu i odpowiedniego ustawienia wskazówek swojego zegara.”

Obliczamy czas pracy serwera nad zapytaniem:

  1. Obliczamy czas drogi pakietu od klienta do serwera: ((Przybycie – Pochodzenie) — (Transmit – Otrzymanie)) / 2
  2. Obliczamy różnicę między czasem klienta a serwera:
    Otrzymanie — Pochodzenie — ((Przybycie – Pochodzenie) — (Transmit – Otrzymanie)) / 2 =
    2 * Otrzymanie — 2 * Pochodzenie – Przybycie + Pochodzenie + Transmit – Otrzymanie =
    Otrzymanie – Pochodzenie – Przybycie + Transmit

Dodajemy uzyskaną wartość do lokalnego czasu i cieszymy się życiem.

Wyjście wyniku

time_different = answer.get_time_different(arrive_time)
result = "Różnica czasu: {}nCzas serwera: {}n{}".format(
    time_different,
    datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
    answer.to_display())
print(result)

Przydatne link.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster