Пишемо простий NTP клієнт

Здрастуйте, хабраюзери. Сьогодні я хочу розповісти про те, як написати свій простенький клієнт NTP. В основному, розмова зайде про структуру пакета і спосіб обробки відповіді з сервера NTP. Код буде написаний на пітоні, тому що, як на мене, кращої мови для подібних речей просто не знайти. Знавці звернуть увагу на схожість коду з кодом ntplib - я надихався саме їм.

Отже, що таке NTP? NTP – протокол взаємодії із серверами точного часу. Цей протокол використовують у багатьох сучасних машинах. Наприклад, служба w32tm у windows.

Усього існує 5 версій NTP протоколу. Перша, 0-я версія (1985 р, RFC958)), на даний момент вважається застарілою. Зараз використовуються новіші, 1-а (1988, RFC1059), 2-я(1989, RFC1119), 3-я(1992, RFC1305) і 4-а(1996, RFC2030). 1-4 версії є сумісними одна з одною, вони відрізняються лише алгоритмами роботи серверів.

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

Пишемо простий NTP клієнт

Leap indicator (індикатор корекції) — число, яке показує попередження про секунду координації. Значення:

  • 0 – немає корекції
  • 1 – остання хвилина дня містить 61 секунду
  • 2 – остання хвилина дня містить 59 секунд
  • 3 – несправність сервера (час не синхронізовано)

Номер версії (Номер версії) – номер версії протоколу NTP (1-4).

режим (режим) – режим роботи відправника пакета. Значення від 0 до 7, найчастіші:

  • 3 – клієнт
  • 4 – сервер
  • 5 – широкомовний режим

Прошарок (Рівень нашарування) - кількість проміжних шарів між сервером і еталонним годинником (1 - сервер бере дані безпосередньо з еталонного годинника, 2 - сервер бере дані з сервера з рівнем 1 і т.д.).
басейн — ціле число зі знаком, що становить максимальний інтервал між послідовними повідомленнями. NTP-клієнт вказує тут інтервал, з яким він має намір опитувати сервер, а NTP-сервер – інтервал, з яким він передбачає, щоб його опитували. Значення дорівнює двійковому логарифму секунд.
Точність (точність) - ціле число зі знаком, що представляє точність системного годинника. Значення дорівнює двійковому логарифму секунд.
Root delay (Затримка сервера) – час, за який показання годинника доходять до NTP-сервера, як число секунд з фіксованою точкою.
Root dispersion (розкид показів сервера) — розкид показів годинника NTP-сервера як число секунд з фіксованою точкою.
Ref 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):
        # 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

Щоб надіслати (і приймати) пакет на сервер, ми повинні вміти перетворювати його на масив байт.
Для цієї (і зворотної) операції напишемо дві функції - 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)

Надсилання пакета на сервер

На сервер необхідно надіслати пакет із заповненими полями версія, режим и Передавати. У Передавати необхідно вказати поточний час на локальній машині (кількість секунд з 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. Знаходимо час шляху пакета від клієнта до сервера: ((Arrive - Originate) - (Transmit - Receive)) / 2
  2. Знаходимо різницю між часом клієнта та сервера:
    Receive - Originate - ((Arrive - Originate) - (Transmit - Receive)) / 2 =
    2 * Receive - 2 * Originate - Arrive + Originate + Transmit - Receive =
    Receive - Originate - Arrive + Transmit

Додаємо отримане значення до локального часу та радіємо життю.

Висновок результату

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)

Корисна посилання.

Джерело: habr.com

Додати коментар або відгук