Escriptura d'un client NTP senzill

Hola habrausers. Avui vull parlar de com escriure el vostre propi client NTP senzill. Bàsicament, la conversa es centrarà en l'estructura del paquet i com es processa la resposta del servidor NTP. El codi s'escriurà en python, perquè, al meu entendre, simplement no hi ha un llenguatge millor per a aquestes coses. Els coneixedors prestaran atenció a la similitud del codi amb el codi ntplib: em vaig "inspirar" en ell.

Aleshores, què és NTP de totes maneres? NTP és un protocol per comunicar-se amb servidors de temps. Aquest protocol s'utilitza en moltes màquines modernes. Per exemple, el servei w32tm a Windows.

Hi ha 5 versions del protocol NTP en total. La primera, la versió 0 (1985, RFC958) es considera actualment obsoleta. Actualment s'utilitzen de més nous, 1r (1988, RFC1059), 2n (1989, RFC1119), 3r (1992, RFC1305) i 4t (1996, RFC2030). Les versions 1-4 són compatibles entre si, només es diferencien en els algorismes dels servidors.

Format de paquet

Escriptura d'un client NTP senzill

Indicador de salt (indicador de correcció) és un número que indica el segon avís intercalat. Significat:

  • 0 - sense correcció
  • 1: l'últim minut del dia conté 61 segons
  • 2: l'últim minut del dia conté 59 segons
  • 3 - error del servidor (temps fora de sincronització)

Número de versió (número de versió): número de versió del protocol NTP (1-4).

manera (mode) — mode de funcionament de l'emissor del paquet. Valor de 0 a 7, més comú:

  • 3 - client
  • 4 - servidor
  • 5 - Mode d'emissió

Estrat (nivell de capes): el nombre de capes intermèdies entre el servidor i el rellotge de referència (1: el servidor pren dades directament del rellotge de referència, 2: el servidor pren dades del servidor amb el nivell 1, etc.).
piscina és un nombre enter amb signe que representa l'interval màxim entre missatges consecutius. El client NTP especifica aquí l'interval en què espera enquestar el servidor, i el servidor NTP especifica l'interval en què espera ser sondejat. El valor és igual al logaritme binari de segons.
Precisió (precisió) és un nombre enter amb signe que representa la precisió del rellotge del sistema. El valor és igual al logaritme binari de segons.
retard de l'arrel (latència del servidor) és el temps que triga el rellotge a arribar al servidor NTP, com a nombre de segons de punt fix.
dispersió arrel (dispersió del servidor): la dispersió del rellotge del servidor NTP com a nombre de segons de punt fix.
Ref id (identificador de la font) - identificador del rellotge. Si el servidor té l'estrat 1, ref id és el nom del rellotge atòmic (4 caràcters ASCII). Si el servidor utilitza un altre servidor, aleshores l'ID de referència conté l'adreça d'aquest servidor.
Els últims 4 camps són el temps - 32 bits - la part entera, 32 bits - la part fraccionària.
Referència - l'últim rellotge del servidor.
Originar – hora en què es va enviar el paquet (emplenat pel servidor; més informació a continuació).
Rebre – hora en què el servidor va rebre el paquet.
Transmetre – hora en què el paquet es va enviar des del servidor al client (emplenat pel client, més informació a continuació).

Els dos últims camps no seran considerats.

Escrivim el nostre paquet:

Codi del paquet

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

Per enviar (i rebre) un paquet al servidor, hem de ser capaços de convertir-lo en una matriu de bytes.
Per a aquesta operació (i inversa), escriurem dues funcions: pack() i unpack():

funció de paquet

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

funció de desempaquetar

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

Per a mandrosos, com a aplicació: codi que converteix el paquet en una bonica cadena

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)

Enviament d'un paquet al servidor

Envieu un paquet amb camps emplenats al servidor version, manera и Transmetre. En Transmetre heu d'especificar l'hora actual a la màquina local (nombre de segons des de l'1 de gener de 1900), versió - qualsevol de l'1-4, mode - 3 (mode client).

El servidor, després de rebre la sol·licitud, omple tots els camps del paquet NTP, copiant-hi el camp Originar valor de Transmetre, que va venir a la petició. És un misteri per a mi per què el client no pot omplir immediatament el valor del seu temps al camp Originar. Com a resultat, quan el paquet torna, el client té 4 valors de temps: el moment en què es va enviar la sol·licitud (Originar), l'hora en què el servidor va rebre la sol·licitud (Rebre), l'hora en què el servidor va enviar la resposta (Transmetre) i el moment de recepció de la resposta per part del client - Arribar (no al paquet). Amb aquests valors podem establir l'hora correcta.

Codi d'enviament i recepció de paquets

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

Tractament de dades des del servidor

El tractament de les dades del servidor és semblant a les accions del senyor anglès de l'antic problema de Raymond M. Smallian (1978): “Una persona no tenia un rellotge de polsera, però a casa hi havia un rellotge de paret precís, que ell de vegades oblidava el vent. Un dia, oblidant-se de tornar a engegar el rellotge, va anar a visitar el seu amic, va passar la nit amb ell i, quan va tornar a casa, va aconseguir posar el rellotge correctament. Com va aconseguir fer-ho si no es coneixia amb antelació el temps de viatge? La resposta és: “En sortir de casa, una persona dona una bossa al rellotge i recorda la posició de les agulles. Arribant a un amic i deixant els convidats, anota l'hora de la seva arribada i sortida. Això li permet saber quant de temps va estar fora. Tornant a casa i mirant el rellotge, una persona determina la durada de la seva absència. Restant d'aquest temps el temps que va passar visitant, la persona descobreix el temps que va passar a la carretera d'anada i tornada. En afegir la meitat del temps passat a la carretera al temps de deixar els convidats, té l'oportunitat de saber l'hora d'arribada a casa i ajustar les agulles del seu rellotge en conseqüència.

Trobeu l'hora en què el servidor treballava en la sol·licitud:

  1. Trobar el temps de viatge del paquet des del client fins al servidor: ((Arriba - Origina) - (Transmet - Rebre)) / 2
  2. Trobeu la diferència entre l'hora del client i del servidor:
    Rebre - Originar - ((Arriba - Originar) - (Transmetre - Rebre)) / 2 =
    2 * Rebre - 2 * Originar - Arribar + Originar + Transmetre - Rebre =
    Rebre - Originar - Arribar + Transmetre

Afegim el valor rebut a l'hora local i gaudim de la vida.

Sortida del resultat

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)

Útil enllaç.

Font: www.habr.com

Afegeix comentari