Escribiendo un cliente NTP simple

Hola habrausuarios. Hoy quiero hablar sobre cómo escribir su propio cliente NTP simple. Básicamente, la conversación girará en torno a la estructura del paquete y cómo se procesa la respuesta del servidor NTP. El código estará escrito en python porque, en mi opinión, simplemente no hay un lenguaje mejor para tales cosas. Los conocedores prestarán atención a la similitud del código con el código ntplib: me "inspiró".

Entonces, ¿qué es NTP de todos modos? NTP es un protocolo para comunicarse con servidores de tiempo. Este protocolo se utiliza en muchas máquinas modernas. Por ejemplo, el servicio w32tm en Windows.

Hay 5 versiones del protocolo NTP en total. La primera, la versión 0 (1985, RFC958) actualmente se considera obsoleta. Actualmente se utilizan los más nuevos, primero (1, RFC1988), segundo (1059, RFC2), tercero (1989, RFC1119) y cuarto (3, RFC1992). Las versiones 1305-4 son compatibles entre sí, solo difieren en los algoritmos de los servidores.

Formato de paquete

Escribiendo un cliente NTP simple

Indicador de salto (indicador de corrección) es un número que indica la advertencia del segundo intercalar. Significado:

  • 0 - sin corrección
  • 1 - el último minuto del día contiene 61 segundos
  • 2 - el último minuto del día contiene 59 segundos
  • 3 - falla del servidor (tiempo fuera de sincronización)

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

Moda (modo) — modo de operación del emisor del paquete. Valor de 0 a 7, más común:

  • 3 - cliente
  • 4 - servidor
  • 5 - modo de transmisión

estrato (nivel de capas) - el número de capas intermedias entre el servidor y el reloj de referencia (1 - el servidor toma datos directamente del reloj de referencia, 2 - el servidor toma datos del servidor con nivel 1, etc.).
Piscina es un entero con signo que representa el intervalo máximo entre mensajes consecutivos. El cliente NTP especifica aquí el intervalo en el que espera sondear el servidor y el servidor NTP especifica el intervalo en el que espera ser sondeado. El valor es igual al logaritmo binario de segundos.
Precisión (precisión) es un entero con signo que representa la precisión del reloj del sistema. El valor es igual al logaritmo binario de segundos.
retardo de raíz (latencia del servidor) es el tiempo que tarda el reloj en llegar al servidor NTP, como un número de segundos de punto fijo.
dispersión de raíces (dispersión del servidor): la dispersión del reloj del servidor NTP como un número de segundos de punto fijo.
Identificación de referencia (identificación de la fuente) – identificación del reloj. Si el servidor tiene el estrato 1, entonces ref id es el nombre del reloj atómico (4 caracteres ASCII). Si el servidor usa otro servidor, la ID de referencia contiene la dirección de este servidor.
Los últimos 4 campos representan el tiempo - 32 bits - la parte entera, 32 bits - la parte fraccionaria.
Referencia - el último reloj en el servidor.
Originar – hora en que se envió el paquete (completado por el servidor; más sobre eso a continuación).
Recibe – hora en que el servidor recibió el paquete.
Transmitir – hora en que se envió el paquete desde el servidor al cliente (completado por el cliente, más sobre eso a continuación).

Los dos últimos campos no serán considerados.

Escribamos nuestro paquete:

Código del paquete

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

Para enviar (y recibir) un paquete al servidor, debemos poder convertirlo en una matriz de bytes.
Para esta operación (y la inversa), escribiremos dos funciones: empaquetar() y desempaquetar():

función de paquete

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ón 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

Para los perezosos, como aplicación: código que convierte el paquete en una hermosa 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)

Envío de un paquete al servidor

Enviar un paquete con campos llenos al servidor Versión, Moda и Transmitir. En Transmitir debe especificar la hora actual en la máquina local (número de segundos desde el 1 de enero de 1900), versión: cualquiera de 1 a 4, modo: 3 (modo cliente).

El servidor, al recibir la solicitud, completa todos los campos del paquete NTP, copiando en el campo Originar valor de Transmitir, que vino en la solicitud. Es un misterio para mí por qué el cliente no puede completar inmediatamente el valor de su tiempo en el campo. Originar. Como resultado, cuando el paquete regresa, el cliente tiene 4 valores de tiempo: la hora en que se envió la solicitud (Originar), la hora en que el servidor recibió la solicitud (Recibe), la hora en que el servidor envió la respuesta (Transmitir) y la hora de recepción de una respuesta por parte del cliente - ¡Disfruta tu viaje! (no en el paquete). Con estos valores podemos establecer la hora correcta.

Código de envío y recepción de paquetes

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

Procesamiento de datos desde el servidor

El procesamiento de datos del servidor es similar a las acciones del caballero inglés del viejo problema de Raymond M. Smallian (1978): “Una persona no tenía reloj de pulsera, pero en casa había un reloj de pared preciso, que a veces olvidaba al viento. Un día, olvidándose de volver a poner en marcha el reloj, fue a visitar a su amigo, pasó la noche con él y, cuando regresó a casa, logró poner en hora el reloj correctamente. ¿Cómo se las arregló para hacer esto si el tiempo de viaje no se sabía de antemano? La respuesta es: “Al salir de casa, una persona da cuerda al reloj y recuerda la posición de las manecillas. Al llegar a un amigo y dejar a los invitados, anota la hora de su llegada y salida. Esto le permite averiguar cuánto tiempo estuvo fuera. Al regresar a casa y mirar el reloj, una persona determina la duración de su ausencia. Restando de este tiempo el tiempo que pasó visitando, la persona averigua el tiempo que pasó en el camino de ida y vuelta. Al sumar la mitad del tiempo que pasa en el camino al tiempo de dejar a los invitados, tiene la oportunidad de averiguar la hora de llegada a casa y ajustar las manecillas de su reloj en consecuencia.

Encuentre la hora en que el servidor estuvo trabajando en la solicitud:

  1. Encontrar el tiempo de viaje del paquete desde el cliente al servidor: ((Llegar - Originar) - (Transmitir - Recibir)) / 2
  2. Encuentre la diferencia entre la hora del cliente y la del servidor:
    Recibir - Originar - ((Llegar - Originar) - (Transmitir - Recibir)) / 2 =
    2 * Recibir - 2 * Originar - Llegar + Originar + Transmitir - Recibir =
    Recibir - Originar - Llegar + Transmitir

Agregamos el valor recibido a la hora local y disfrutamos de la vida.

Salida de resultados

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 enlace.

Fuente: habr.com

Añadir un comentario