Pisanie prostego klienta NTP

Witam habrauserów. Dzisiaj chcę porozmawiać o tym, jak napisać własnego prostego klienta NTP. Zasadniczo rozmowa skręci na strukturę pakietu i sposób przetwarzania odpowiedzi z serwera NTP. Kod będzie napisany w pythonie, bo moim zdaniem po prostu nie ma lepszego języka do takich rzeczy. Koneserzy zwrócą uwagę na podobieństwo kodu z kodem ntplib – ja się nim „zainspirowałem”.

Czym w ogóle jest NTP? NTP to protokół do komunikacji z serwerami czasu. Ten protokół jest używany w wielu nowoczesnych maszynach. Na przykład usługa w32tm w systemie Windows.

W sumie istnieje 5 wersji protokołu NTP. Pierwsza, wersja 0 (1985, RFC958) jest obecnie uważana za przestarzałą. Obecnie używane są nowsze, 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 serwerów.

Format pakietu

Pisanie prostego klienta NTP

Wskaźnik skoku (wskaźnik korekty) to liczba wskazująca ostrzeżenie o sekundzie przestępnej. Oznaczający:

  • 0 - brak korekty
  • 1 - ostatnia minuta dnia zawiera 61 sekund
  • 2 - ostatnia minuta dnia zawiera 59 sekund
  • 3 - awaria serwera (brak synchronizacji czasu)

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

Moda (tryb) — tryb pracy nadawcy pakietu. Wartość od 0 do 7, najczęściej:

  • 3 - klient
  • 4 - serwer
  • 5 - tryb nadawania

Warstwa (layering level) - ilość warstw pośrednich pomiędzy serwerem a zegarem referencyjnym (1 - serwer pobiera dane bezpośrednio z zegara referencyjnego, 2 - serwer pobiera dane z serwera z poziomem 1, itd.).
pływacki jest liczbą całkowitą ze znakiem reprezentującą maksymalny odstęp między kolejnymi komunikatami. Klient NTP określa tutaj interwał, w jakim spodziewa się odpytywać serwer, a serwer NTP określa interwał, w jakim oczekuje odpytywania. Wartość jest równa binarnemu logarytmowi sekund.
Detaliczność (precyzja) to liczba całkowita ze znakiem reprezentująca dokładność zegara systemowego. Wartość jest równa binarnemu logarytmowi sekund.
opóźnienie roota (opóźnienie serwera) to czas potrzebny zegarowi na dotarcie do serwera NTP, jako stała liczba sekund.
dyspersja korzeni (server scatter) - Rozrzut zegara serwera NTP jako stała liczba sekund.
Identyfikator ref (identyfikator źródła) – identyfikator zegarka. Jeśli serwer ma warstwę 1, to ref id jest nazwą zegara atomowego (4 znaki ASCII). Jeśli serwer korzysta z innego serwera, to ref id zawiera adres tego serwera.
Ostatnie 4 pola reprezentują czas - 32 bity - część całkowita, 32 bity - część ułamkowa.
Numer Referencyjny - najnowszy zegar na serwerze.
Pochodzić – czas wysłania pakietu (wypełnia serwer – więcej o tym poniżej).
Otrzymać – czas odebrania pakietu przez serwer.
Transmitować – czas wysłania pakietu z serwera do klienta (wypełnia klient, więcej o tym poniżej).

Dwa ostatnie pola nie będą brane pod uwagę.

Napiszmy nasz pakiet:

Kod pakietu

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

Aby wysłać (i odebrać) pakiet do serwera, musimy być w stanie przekształcić go w tablicę bajtów.
Dla tej (i odwrotnej) operacji napiszemy dwie funkcje - pack() i unpack():

funkcja pakowania

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 rozpakowywania

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

Dla leniwych, jako aplikacja - kod zamieniający paczkę w piękny string

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)

Wysyłanie paczki na serwer

Wyślij pakiet z wypełnionymi polami na serwer Wersja, Moda и Transmitować, Transmitować musisz podać aktualny czas na lokalnej maszynie (liczba sekund od 1 stycznia 1900), wersja - dowolna z 1-4, tryb - 3 (tryb klienta).

Serwer po otrzymaniu żądania wypełnia wszystkie pola w pakiecie NTP, kopiując do pola Pochodzić wartość od Transmitować, który pojawił się w żądaniu. Jest dla mnie zagadką, dlaczego klient nie może od razu wypełnić wartości swojego czasu w terenie Pochodzić. W rezultacie, gdy pakiet wraca, klient ma 4 wartości czasowe – czas wysłania żądania (Pochodzić), czas otrzymania żądania przez serwer (Otrzymać), czas wysłania odpowiedzi przez serwer (Transmitować) i czas otrzymania odpowiedzi przez klienta - Przybyć (brak w opakowaniu). Dzięki tym wartościom możemy ustawić prawidłowy czas.

Kod wysyłania i odbierania paczki

# 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 podobne do działań angielskiego dżentelmena ze starego problemu Raymonda M. Smalliana (1978): „Jedna osoba nie miała zegarka na rękę, ale w domu był dokładny zegar ścienny, który czasami zapomniałem nakręcić. Pewnego dnia, zapominając o ponownym uruchomieniu zegara, poszedł odwiedzić przyjaciela, spędził z nim wieczór, a kiedy wrócił do domu, zdążył poprawnie ustawić zegar. Jak mu się to udało, skoro czas podróży nie był z góry znany? Odpowiedź brzmi: „Wychodząc z domu, człowiek nakręca zegar i zapamiętuje położenie wskazówek. Przychodząc do przyjaciela i wychodząc z gości, zapisuje godzinę swojego przyjazdu i wyjazdu. To pozwala mu dowiedzieć się, jak długo go nie było. Wracając do domu i patrząc na zegar, osoba określa czas swojej nieobecności. Odejmując od tego czasu czas, który spędził na zwiedzaniu, osoba dowiaduje się, ile czasu spędził na drodze tam iz powrotem. Dodając połowę czasu spędzonego w drodze do czasu wyjścia z gości, otrzymuje możliwość poznania godziny przyjazdu do domu i odpowiedniego ustawienia wskazówek zegara.

Znajdź czas, w którym serwer pracował nad żądaniem:

  1. Znalezienie czasu podróży pakietu od klienta do serwera: ((Przybycie - Pochodzenie) - (Przesyłanie - Odbiór)) / 2
  2. Znajdź różnicę między czasem klienta i serwera:
    Odbiór - Pochodzenie - ((Przybycie - Pochodzenie) - (Przesyłanie - Odbiór)) / 2 =
    2 * Odbiór - 2 * Początek - Przyjazd + Początek + Transmisja - Odbiór =
    Odbiór - Pochodzenie - Przyjazd + Transmisja

Otrzymaną wartość dodajemy do czasu lokalnego i cieszymy się życiem.

Wyjście wyniku

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)

Użyteczne łącze.

Źródło: www.habr.com

Dodaj komentarz