Пішам просты 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

Дадаць каментар