Пишем прост NTP клиент.

Здравейте, хабраюзери. Днес ще ви разкажа как да напишете прост NTP клиент. Основно ще обсъдим структурата на пакета и начина на обработка на отговора от NTP сървъра. Кодът ще бъде написан на Python, защото, както ми се струва, няма по-подходящ език за подобни неща. Запознати ще забележат сходството с кода на ntplib — именно той ме вдъхнови.

И така, какво всъщност представлява NTP? NTP е протокол за взаимодействие със сървъри за прецизно време. Този протокол се използва в много съвременни устройства. Например, услугата w32tm в Windows.

Съществуват общо 5 версии на NTP протокола. Първата, нулевата версия (1985 г., RFC958), в момента се счита за остаряла. В момента се използват по-нови, 1-ва (1988, RFC1059), 2-ра(1989, RFC1119), 3-та(1992, RFC1305) и 4-та(1996, RFC2030). Версиите 1-4 са съвместими помежду си, те се различават само по алгоритмите на работа. сървъри.

Формат на пакета

Пишем прост NTP клиент.

Индикатор за скок (индикатор на корекция) — число, което показва предупреждение за секунда на координация. Стойност:

  • 0 – без корекция
  • 1 – последната минута на деня съдържа 61 секунда
  • 2 – последната минута на деня съдържа 59 секунди
  • 3 – неизправност на сървъра (времето не е синхронизирано)

Номер на версия (номер на версия) – номер на версията на NTP протокола (1-4).

Режим (режим) — режим на работа на изпращача на пакета. Стойност от 0 до 7, най-чести:

  • 3 – клиент
  • 4 – сървър
  • 5 – широковещателен режим

Стратум (ниво на наслояване) – брой междинни слоеве между сървъра и референтните часовници (1 – сървърът получава данните директно от референтните часовници, 2 – сървърът получава данни от сървър с ниво 1 и т.н.).
Пул — цяло число със знак, представляващо максималния интервал между последователните съобщения. NTP клиентът посочва тук интервала, с който планира да запитва сървъра, а NTP сървърът – интервала, с който планира да бъде запитван. Стойността е равна на двоичния логаритъм на секундите.
Прецизност (точност) — цяло число със знак, представляващо точността на системните часовници. Стойността е равна на двоичния логаритъм на секундите.
Коренна забавяне (забавяне на сървъра) – времето, за което показанията на часовниците достигат до NTP сървъра, записано като число секунди с фиксирана запетая.
Коренна дисперсия (разброс на показанията на сървъра) — разбросът в показанията на NTP сървъра, записан като число секунди с фиксирана запетая.
Референтен ID (идентификатор на източника) – id на часовника. Ако сървърът има стратум 1, то ref id – името на атомните часовници (4 ASCII символа). Ако сървърът използва друг сървър, то в ref id е записан адресът на този сървър.
Последните 4 полета представляват време – 32 бита – цяло число, 32 бита – дробна част.
Справка — последните показания на часовника на сървъра.
Произход – времето, когато пакета е изпратен (попълва се от сървъра – повече за това по-долу).
Получаване – времето на получаване на пакета от сървъра.
Изпращане – времето на изпращане на пакета от сървъра към клиента (попълва се от клиента, повече за това също по-долу).

Двете последни полета няма да разглеждаме.

Нека напишем нашия пакет:

Код на пакета

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

    def __init__(self, version_number=2, mode=3, transmit=0):
        # Необходимо е за влизане на лийп секунда (2 бита)
        self.leap_indicator = 0
        # Версия на протокола (3 бита)
        self.version_number = version_number
        # Режим на изпращача (3 бита)
        self.mode = mode
        # Нивото на "слоестост" четене на времето (1 байт)
        self.stratum = 0
        # Интервал между заявките (1 байт)
        self.pool = 0
        # Прецизност (log2) (1 байт)
        self.precision = 0
        # Интервал за достигане на NTP сървера (4 байта)
        self.root_delay = 0
        # Разсейване на часа на NTP-сървера (4 байта)
        self.root_dispersion = 0
        # Индикатор на часовниците (4 байта)
        self.ref_id = 0
        # Последно обновление време на сървера (8 байта)
        self.reference = 0
        # Време на изпращане на пакета от локалната машина (8 байта)
        self.originate = 0
        # Време на получаване на сървъра (8 байта)
        self.receive = 0
        # Време на изпращане на отговор от сървера (8 байта)
        self.transmit = transmit

За да изпращаме (и приемаме) пакет на сървъра, трябва да можем да го преобразуваме в масив от байтове.
За тази (и обратната) операция ще напишем две функции – pack() и unpack():

Функция 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))

Функция unpack

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

За мързеливите, в допълнение – код, който превръща пакета в елегантен низ

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)

Изпращане на пакета на сървъра

На сървъра трябва да се изпрати пакет с попълнени полета Version, Режим и Изпращане. В Изпращане необходимо е да се укаже текущото време на локалната машина (броя на секундите от 1 януари 1900 г), версия – всяка от 1 до 4, режим – 3 (клиентски режим).

Сървърът, който е приел заявката, попълва всички полета в NTP пакета, копирайки в полето Произход значението от Изпращане, получено в заявката. За мен е загадка защо клиентът не може да попълни веднага стойността на своето време в полето Произход. В крайна сметка, когато пакета се върне, клиентът разполага с 4 стойности на време – време на изпращане на заявката (Произход), време на получаване на заявката от сървъра (Получаване), време на изпращане на отговора от сървъра (Изпращане) и време на получаване на отговора от клиента – Пристига (не е в пакета). С тези стойности можем да установим точното време.

Код за изпращане и получаване на пакета

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

Обработка на данни от сървъра

Обработката на данни от сървъра наподобява действията на английски джентълмен от старата задача на Реймънд М. Смаллиан (1978): «Един човек нямаше ръчни часовници, но у дома му висеха точни стенни часовници, които понякога забравяше да навива. Веднъж, забравяйки за пореден път да навие часовниците, той отиде на гости на приятеля си, прекара при него вечер, а след като се върна у дома, успя да настрои часовниците. Как успя да направи това, ако времето в пътя предварително не беше известно?» Отговорът е: «Излизайки от дома, човекът навива часовниците и запомня в какво положение са стрелките. Като пристига при приятеля си и си тръгва от там, той отбелязва времето на пристигане и напускане. Това му позволява да узнае колко време е прекарал на гости. Връщайки се у дома и поглеждайки часовниците, човекът определя продължителността на отсъствието си. Изваждайки от това време времето, прекарано на гости, той узнава времето, прекарано за пътуването дотам и обратно. Добавяйки към времето на напускане на гостите половината от времето, изразходвано за пътуване, той получава възможността да узнае времето на пристигане у дома и да премести стрелките на часовниците си съответно.»

Намираме времето на работа на сървъра над заявката:

  1. Намираме времето за пътуване на пакета от клиента до сървъра: ((Пристигане – Произход) — (Предаване – Получаване)) / 2
  2. Намираме разликата между времето на клиента и времето на сървъра:
    Получаване — Произход — ((Пристигане – Произход) — (Предаване – Получаване)) / 2 =
    2 * Получаване — 2 * Произход – Пристигане + Произход + Предаване – Получаване =
    Получаване – Произход – Пристигане + Предаване

Добавяме полученото значение към локалното време и се радваме на живота.

Извеждане на резултата

time_different = answer.get_time_different(arrive_time)
result = "Разлика във времето: {}nВреме на сървъра: {}n{}".format(
    time_different,
    datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
    answer.to_display())
print(result)

Полезно линк.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster