Psaní jednoduchého NTP klienta

Ahoj Habrauseři. Dnes chci mluvit o tom, jak napsat vlastního jednoduchého NTP klienta. V zásadě se konverzace stočí ke struktuře paketu a způsobu zpracování odpovědi z NTP serveru. Kód bude napsán v Pythonu, protože se mi zdá, že na takové věci prostě neexistuje lepší jazyk. Znalci si všimnou podobnosti kódu s kódem ntplib - byl jsem jím „inspirován“.

Co je tedy vlastně NTP? NTP je protokol pro interakci s přesnými časovými servery. Tento protokol se používá v mnoha moderních strojích. Například služba w32tm ve Windows.

Existuje celkem 5 verzí protokolu NTP. První, verze 0 (1985, RFC958)), je v současné době považována za zastaralou. Nyní se používají novější, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) a 4. (1996, RFC2030). Verze 1-4 jsou vzájemně kompatibilní, liší se pouze v algoritmech provozu serveru.

Formát balíčku

Psaní jednoduchého NTP klienta

Indikátor skoku (indikátor opravy) - číslo označující upozornění na koordinační sekundu. Význam:

  • 0 – žádná oprava
  • 1 – poslední minuta dne obsahuje 61 sekund
  • 2 – poslední minuta dne obsahuje 59 sekund
  • 3 – porucha serveru (čas není synchronizován)

Číslo verze (číslo verze) – číslo verze protokolu NTP (1-4).

režim (mode) — provozní režim odesílatele paketů. Hodnota od 0 do 7, nejběžnější:

  • 3 – klient
  • 4 – server
  • 5 – režim vysílání

Vrstva (úroveň vrstvení) – počet mezivrstev mezi serverem a referenčními hodinami (1 – server bere data přímo z referenčních hodin, 2 – server bere data ze serveru s vrstvou 1 atd.).
Bazén je celé číslo se znaménkem představující maximální interval mezi po sobě jdoucími zprávami. NTP klient zde specifikuje interval, ve kterém očekává dotazování serveru, a NTP server specifikuje interval, ve kterém očekává, že bude dotazován. Hodnota je rovna binárnímu logaritmu sekund.
Přesnost (přesnost) je celé číslo se znaménkem představující přesnost systémových hodin. Hodnota je rovna binárnímu logaritmu sekund.
Zpoždění kořene (server delay) – čas, který trvá, než naměřené hodiny dosáhnou NTP server, jako počet sekund s pevnou čárkou.
Kořenová disperze (server spread) - rozložení hodin NTP serveru jako počet sekund s pevným bodem.
Ref id (identifikátor zdroje) – id hodin. Pokud má server vrstvu 1, pak ref id je název atomových hodin (4 znaky ASCII). Pokud server používá jiný server, pak ref id obsahuje adresu tohoto serveru.
Poslední 4 pole představují čas - 32 bitů - celočíselná část, 32 bitů - zlomková část.
Odkaz — poslední odečty hodin na serveru.
Pocházejí – čas, kdy byl paket odeslán (vyplněno serverem – více o tom níže).
Dostávat – čas, kdy byl paket přijat serverem.
Předat – čas odeslání paketu ze serveru klientovi (vyplňuje klient, více o tom níže).

Poslední dvě pole nebudeme uvažovat.

Pojďme napsat náš balíček:

Kód balíčku

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

Abychom mohli odeslat (a přijmout) paket na server, musíme být schopni jej přeměnit na bajtové pole.
Pro tuto (a obrácenou) operaci napíšeme dvě funkce - pack() a unpack():

funkce balení

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

funkce rozbalení

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

Pro lenochy jako aplikace - kód, který promění balíček v krásný řetězec

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)

Odeslání balíčku na server

Paket s vyplněnými poli musí být odeslán na server Verze, režim и Předat. V Předat musíte zadat aktuální čas na místním počítači (počet sekund od 1. ledna 1900), verze - libovolná z 1-4, režim - 3 (klientský režim).

Server po přijetí požadavku vyplní všechna pole v paketu NTP a zkopíruje je do pole Pocházejí hodnota od Předat, který přišel v žádosti. Je mi záhadou, proč klient nemůže okamžitě vyplnit hodnotu svého času v terénu Pocházejí. Výsledkem je, že když se paket vrátí, klient má 4 časové hodnoty - čas odeslání požadavku (Pocházejí), čas, kdy server přijal požadavek (Dostávat), čas, kdy server odeslal odpověď (Předat) a čas, kdy klient obdržel odpověď – Přijet (není v balení). Pomocí těchto hodnot můžeme nastavit správný čas.

Kód pro odeslání a příjem balíku

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

Zpracování dat ze serveru

Zpracování dat ze serveru je podobné jednání anglického gentlemana ze starého problému Raymonda M. Smullyana (1978): „Jeden člověk neměl náramkové hodinky, ale doma měl přesné nástěnné hodiny, které občas zapomněl větrat. Jednoho dne, když si zapomněl znovu natáhnout hodinky, šel navštívit svého přítele, strávil s ním večer, a když se vrátil domů, podařilo se mu hodinky správně nastavit. Jak se mu to podařilo, když čas cesty nebyl předem znám? Odpověď zní: „Člověk si při odchodu z domova natahuje hodinky a pamatuje si, v jaké poloze jsou ručičky. Poté, co přišel k příteli a opustil hosty, zaznamenává čas svého příjezdu a odjezdu. To mu umožňuje zjistit, jak dlouho byl na návštěvě. Po návratu domů a při pohledu na hodiny člověk určuje dobu své nepřítomnosti. Odečtením času stráveného návštěvou od tohoto času člověk zjistí čas strávený cestováním tam a zpět. Přičtením poloviny času stráveného na cestě k času odjezdu hostů získá možnost zjistit čas příjezdu domů a podle toho upravit ručičky hodinek.“

Najděte čas, kdy server pracuje na požadavku:

  1. Najděte dobu cesty paketu z klienta na server: ((Přijít – Vytvořit) – (Předat – Přijmout)) / 2
  2. Najděte rozdíl mezi časem klienta a serveru:
    Přijmout – Počátek – ((Příchod – Počátek) – (Odeslat – Přijmout)) / 2 =
    2 * Příjem – 2 * Počátek – Příjezd + Počátek + Odeslání – Příjem =
    Příjem – Vznik – Příjezd + Odeslání

Výslednou hodnotu přidáme k místnímu času a užíváme si života.

Výstup výsledku

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žitečný odkaz.

Zdroj: www.habr.com

Přidat komentář