Scriem un client NTP simplu

Bună, utilizatori de Habr. Astăzi vreau să vă povestesc despre cum să scrieți un client NTP simplu. Principalul subiect va fi structura pachetului și modul de procesare a răspunsului de la serverul NTP. Codul va fi scris în Python, pentru că, după părerea mea, nu există un alt limbaj mai bun pentru astfel de lucruri. Cunoștințele vor observa asemănarea codului cu cel din ntplib — m-am „inspirat” exact din el.

Deci, ce este NTP? NTP este un protocol de interacțiune cu serverele de timp precis. Acest protocol este folosit în multe mașini moderne. De exemplu, serviciul w32tm în Windows.

Există în total 5 versiuni ale protocolului NTP. Prima, versiunea 0 (1985, RFC958), este considerată în prezent învechită. Acum sunt folosite versiuni mai noi, versiunea 1 (1988, RFC1059), versiunea 2 (1989, RFC1119), versiunea 3 (1992, RFC1305) și versiunea 4 (1996, RFC2030). Versiunile 1-4 sunt compatibile între ele, ele diferă doar prin algoritmii de funcționare. servere.

Formatul pachetului

Scriem un client NTP simplu

Indicator de leap (indicator de corecție) — un număr care arată un avertisment despre o secundă de coordonare. Valoare:

  • 0 – fără corecție
  • 1 – ultima minută a zilei conține 61 de secunde
  • 2 – ultima minută a zilei conține 59 de secunde
  • 3 – defectul serverului (timpul nu este sincronizat)

Numărul versiunii (numărul versiunii) – numărul versiunii protocolului NTP (1-4).

Mod (mod) — modul de funcționare al expeditorului pachetului. Valoare de la 0 la 7, cele mai frecvente:

  • 3 – client
  • 4 – server
  • 5 – modul de difuzare

Stratum (nivel de stratificare) – numărul de straturi intermediare între server și ceasurile de referință (1 – serverul obține date direct de la ceasurile de referință, 2 – serverul obține date de la un server de nivel 1 etc.).
Pool — un număr întreg semnat care reprezintă intervalul maxim între mesajele consecutive. Clientul NTP specifică aici intervalul cu care plănuiește să interogheze serverul, iar serverul NTP – intervalul cu care își propune să fie interogat. Valoarea este egală cu logaritmul binar al secundelor.
Precizie (precizie) — un număr întreg semnat care reprezintă precizia orelor de sistem. Valoarea este egală cu logaritmul binar al secundelor.
Întârziere rădăcină (întârzierea serverului) – timpul necesar ca citirile orelor să ajungă la serverul NTP, ca număr de secunde cu punct fix.
Dispersie rădăcină (dispersia citirilor serverului) — dispersia dusă de timpii serverului NTP ca număr de secunde cu punct fix.
ID de referință (identificatorul sursei) – id-ul ceasului. Dacă serverul are stratum 1, atunci ref id – numele ceasurilor atomice (4 simboluri ASCII). Dacă serverul folosește un alt server, atunci ref id conține adresa acestui server.
Ultimele 4 câmpuri reprezintă timpul – 32 de biți – partea întreagă, 32 de biți – partea fracționară.
Referință — ultimele citiri ale ceasului de pe server.
Origine – timpul când pachetul a fost trimis (completat de server – despre asta mai jos).
Recepție – timpul când pachetul a fost primit de server.
Transmitere – timpul trimiterii pachetului de pe server către client (completat de client, despre asta și mai jos).

Nu vom considera ultimele două câmpuri.

Să scriem pachetul nostru:

Codul pachetului

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

    def __init__(self, version_number=2, mode=3, transmit=0):
        # Necesitatea de a introduce secundele intercalate (2 biți)
        self.leap_indicator = 0
        # Versiunea protocolului (3 biți)
        self.version_number = version_number
        # Mod de trimis (3 biți)
        self.mode = mode
        # Nivelul citirii timpului "stratificării" (1 byte)
        self.stratum = 0
        # Interval între cereri (1 byte)
        self.pool = 0
        # Precizia (log2) (1 byte)
        self.precision = 0
        # Interval pentru sincronizarea ceasului cu serverul NTP (4 bytes)
        self.root_delay = 0
        # Împrăștierea ceasului serverului NTP (4 bytes)
        self.root_dispersion = 0
        # Indicatorul ceasurilor (4 bytes)
        self.ref_id = 0
        # Ultima actualizare a timpului pe server (8 bytes)
        self.reference = 0
        # Timpul trimiterii pachetului de pe mașina locală (8 bytes)
        self.originate = 0
        # Timpul recepționării pe server (8 bytes)
        self.receive = 0
        # Timpul trimiterii răspunsului de către server (8 bytes)
        self.transmit = transmit

Pentru a trimite (și a primi) un pachet pe server, trebuie să fim capabili să-l transformăm într-un array de octeți.
Pentru această (și invers) operație, vom scrie două funcții — pack() și unpack():

Funcția 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))

Funcția 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

Pentru leneși, ca aplicație – cod care transformă pachetul într-un șir frumos

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)

Trimiterea pachetului către server

Pachetul trebuie trimis la server cu câmpurile completate Version, Mod și Transmitere. În Transmitere trebuie să specificăm timpul curent pe mașina locală (numărul de secunde de la 1 ianuarie 1900), versiunea – orice din 1-4, modul – 3 (modul client).

Serverul, odată ce a primit cererea, completează toate câmpurile în pachetul NTP, copind în câmp Origine valoarea din Transmitere, sosită în cerere. Este o enigmă pentru mine de ce clientul nu poate completa imediat valoarea timpului său în câmp Origine. În cele din urmă, când pachetul revine, clientul are 4 valori de timp – timpul trimiterii cererii (Origine), timpul de primire a cererii de către server (Recepție), timpul trimiterii răspunsului de către server (Transmitere) și timpul de primire a răspunsului de către client – Sosește (nu este în pachet). Cu aceste valori putem stabili timpul corect.

Codul de trimitere și primire a pachetului

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

Prelucrarea datelor de la server

Prelucrarea datelor de pe server este similară cu faptele unui gentleman englez din vechea problemă a lui Raymond M. Smullyan (1978): „Unui om nu îi erau ceasuri de mână, dar acasă avea ceasuri de perete precise, pe care uneori le-u uitat să le reîntrețin. Într-o zi, uitând din nou să își pună ceasurile, s-a dus în vizită la un prieten, a petrecut seara acolo, iar când s-a întors acasă, a reușit să își corecteze ceasurile. Cum a reușit să facă acest lucru, dacă timpul de călătorie nu era cunoscut dinainte?” Răspunsul este: „Ieşind din casă, omul își pune ceasurile și își amintește în ce poziție se află aceștia. Ajungând la prietenul său și plecând de la el, își notează ora sosirii și a plecării. Acest lucru îi permite să afle cât timp a petrecut în vizită. Revenind acasă și uitându-se la ceasuri, omul determină durata absenței sale. Scăzând din acest timp timpul petrecut în vizită, el află timpul necesar pentru a ajunge la destinație și întoarcere. Adunând la ora de plecare de la vizită jumătate din timpul necesar pentru a ajunge, el reușește să afle ora de sosire acasă și să ajusteze ceasurile în consecință.”

Calculăm timpul de lucru al serverului pe cerere:

  1. Calculăm timpul de drumul pachetului de la client la server: ((Sosire – Origine) — (Transmit – Primește)) / 2
  2. Calculăm diferența dintre timpul clientului și server:
    Primește — Origine — ((Sosire – Origine) — (Transmit – Primește)) / 2 =
    2 * Primește — 2 * Origine – Sosire + Origine + Transmit – Primește =
    Primește – Origine – Sosire + Transmit

Adăugăm valoarea obținută la timpul local și ne bucurăm de viață.

Afișăm rezultatul

timp_diferent = answer.get_time_different(arrive_time)
result = "Diferența de timp: {}nTimp server: {}n{}".format(
    timp_diferent,
    datetime.datetime.fromtimestamp(time.time() + timp_diferent).strftime("%c"),
    answer.to_display())
print(result)

Utilă linkul.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster