Scrierea unui client NTP simplu

Salutare habrausers. Astăzi vreau să vă vorbesc despre cum să vă scrieți propriul client NTP simplu. Practic, conversația se va îndrepta către structura pachetului și modul în care este procesat răspunsul de la serverul NTP. Codul va fi scris în python, pentru că, în opinia mea, pur și simplu nu există un limbaj mai bun pentru astfel de lucruri. Cunoscătorii vor acorda atenție asemănării codului cu codul ntplib - m-am „inspirat” de el.

Deci, ce este NTP? NTP este un protocol pentru comunicarea cu serverele de timp. Acest protocol este utilizat în multe mașini moderne. De exemplu, serviciul w32tm pe Windows.

Există 5 versiuni ale protocolului NTP în total. Prima, versiunea 0 (1985, RFC958) este considerată în prezent învechită. Cele mai noi sunt utilizate în prezent, primul (1, RFC1988), al doilea (1059, RFC2), al treilea (1989, RFC1119) și al patrulea (3, RFC1992). Versiunile 1305-4 sunt compatibile între ele, diferă doar prin algoritmii serverelor.

Format pachet

Scrierea unui client NTP simplu

Indicator de salt (indicatorul de corecție) este un număr care indică avertismentul secund. Sens:

  • 0 - nicio corecție
  • 1 - ultimul minut al zilei conține 61 de secunde
  • 2 - ultimul minut al zilei conține 59 de secunde
  • 3 - eșec de server (time out of sync)

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

mod (mod) — modul de operare al expeditorului pachetului. Valoare de la 0 la 7, cel mai frecvent:

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

Strat (nivel de stratificare) - numărul de straturi intermediare dintre server și ceasul de referință (1 - serverul preia date direct de la ceasul de referință, 2 - serverul preia date de la serverul cu nivelul 1 etc.).
Piscină este un întreg cu semn care reprezintă intervalul maxim dintre mesajele consecutive. Clientul NTP specifică aici intervalul la care se așteaptă să interogheze serverul, iar serverul NTP specifică intervalul la care se așteaptă să fie interogat. Valoarea este egală cu logaritmul binar al secundelor.
Precizie (precizia) este un număr întreg cu semn care reprezintă acuratețea ceasului sistemului. Valoarea este egală cu logaritmul binar al secundelor.
întârziere la rădăcină (latența serverului) este timpul necesar pentru ca ceasul să ajungă la serverul NTP, ca număr fix de secunde.
dispersie rădăcină (server scatter) - dispersia ceasului serverului NTP ca număr fix de secunde.
ID ref (ID sursă) – ID ceas. Dacă serverul are stratul 1, atunci ref id este numele ceasului atomic (4 caractere ASCII). Dacă serverul folosește un alt server, atunci codul de referință conține adresa acestui server.
Ultimele 4 câmpuri sunt timpul - 32 de biți - partea întreagă, 32 de biți - partea fracțională.
Referinţă - cel mai recent ceas de pe server.
Proveniți – ora la care a fost trimis pachetul (completat de server – mai multe despre asta mai jos).
A primi – ora la care pachetul a fost primit de către server.
Transmite – ora la care pachetul a fost trimis de la server la client (completat de client, mai multe despre asta mai jos).

Ultimele două câmpuri nu vor fi luate în considerare.

Să scriem pachetul nostru:

Cod pachet

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

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

funcția de pachet

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 de despachetare

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 unui pachet către server

Trimiteți un pachet cu câmpuri completate către server Versiune, mod и Transmite. În Transmite trebuie să specificați ora curentă pe mașina locală (număr de secunde de la 1 ianuarie 1900), versiunea - oricare dintre 1-4, modul - 3 (modul client).

Serverul, după ce a primit cererea, completează toate câmpurile din pachetul NTP, copiend în câmp Proveniți valoare de la Transmite, care a venit în cerere. Pentru mine este un mister de ce clientul nu poate completa imediat valoarea timpului său în domeniu Proveniți. Ca urmare, atunci când pachetul revine, clientul are 4 valori de timp - momentul în care a fost trimisă cererea (Proveniți), ora la care serverul a primit cererea (A primi), ora la care serverul a trimis răspunsul (Transmite) și momentul primirii unui răspuns de către client - Data sosirii (nu in pachet). Cu aceste valori putem seta ora corectă.

Cod 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 pe server

Prelucrarea datelor de pe server este similară cu acțiunile domnului englez din vechea problemă a lui Raymond M. Smallian (1978): „O persoană nu avea ceas de mână, dar acasă era un ceas de perete precis, pe care îl uneori uitam să vânt. Într-o zi, uitând să pornească din nou ceasul, s-a dus să-și viziteze prietenul, și-a petrecut seara cu el, iar când s-a întors acasă, a reușit să pună corect ceasul. Cum a reușit să facă asta dacă timpul de călătorie nu era cunoscut dinainte? Răspunsul este: „Ieșind din casă, o persoană dă drumul la ceas și își amintește poziția acelui. Venind la un prieten și părăsind oaspeții, el notează ora sosirii și plecării sale. Acest lucru îi permite să afle cât timp a fost plecat. Întorcându-se acasă și privind la ceas, o persoană determină durata absenței sale. Scăzând din acest timp timpul petrecut în vizită, persoana află timpul petrecut pe drum dus-întors. Adăugând jumătate din timpul petrecut pe drum la timpul părăsirii oaspeților, el are ocazia de a afla ora sosirii acasă și de a-și regla acționările ceasului în consecință.

Găsiți ora la care serverul a lucrat la cerere:

  1. Găsirea timpului de călătorie a pachetului de la client la server: ((Sosire - Originare) - (Transmitere - Primire)) / 2
  2. Găsiți diferența dintre timpul client și cel al serverului:
    Primire - Originare - ((Sosire - Originare) - (Transmitere - Primire)) / 2 =
    2 * Primire - 2 * Originare - Sosire + Originare + Transmitere - Primire =
    Primire - Originare - Sosire + Transmitere

Adăugăm valoarea primită la ora locală și ne bucurăm de viață.

Ieșire rezultat

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)

Util legătură.

Sursa: www.habr.com

Adauga un comentariu