Escribindo un cliente NTP sinxelo

Ola, Habrausers. Hoxe quero falar sobre como escribir o seu propio cliente NTP sinxelo. Basicamente, a conversa irá á estrutura do paquete e ao método de procesar a resposta do servidor NTP. O código escribirase en Python, porque me parece que simplemente non hai mellor linguaxe para tales cousas. Os coñecedores notarán a semellanza do código co código ntplib: eu estaba "inspirado" por el.

Entón, que é exactamente NTP? NTP é un protocolo para a interacción cos servidores de hora exacta. Este protocolo úsase en moitas máquinas modernas. Por exemplo, o servizo w32tm en Windows.

Hai 5 versións do protocolo NTP en total. A primeira, a versión 0 (1985, RFC958)), actualmente considérase obsoleta. Agora utilízanse os máis novos, 1o (1988, RFC1059), 2o (1989, RFC1119), 3o (1992, RFC1305) e 4o (1996, RFC2030). As versións 1-4 son compatibles entre si; só difiren nos algoritmos de operación do servidor.

Formato do paquete

Escribindo un cliente NTP sinxelo

Indicador de salto (indicador de corrección) - un número que indica unha advertencia sobre o segundo de coordinación. Significado:

  • 0 - sen corrección
  • 1: o último minuto do día contén 61 segundos
  • 2: o último minuto do día contén 59 segundos
  • 3: mal funcionamento do servidor (o tempo non está sincronizado)

Número de versión (número de versión) – Número de versión do protocolo NTP (1-4).

modo (mode) — modo operativo do remitente do paquete. Valor de 0 a 7, o máis común:

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

Estrato (nivel de capas) – o número de capas intermedias entre o servidor e o reloxo de referencia (1 – o servidor toma datos directamente do reloxo de referencia, 2 – o servidor toma datos dun servidor coa capa 1, etc.).
Piscina é un enteiro con signo que representa o intervalo máximo entre mensaxes consecutivas. O cliente NTP especifica aquí o intervalo no que espera sondear o servidor, e o servidor NTP especifica o intervalo no que espera ser sondeado. O valor é igual ao logaritmo binario de segundos.
Precisión (precisión) é un enteiro con signo que representa a precisión do reloxo do sistema. O valor é igual ao logaritmo binario de segundos.
Retraso de raíz (atraso do servidor): o tempo que tardan as lecturas do reloxo en chegar ao servidor NTP, como un número de segundos de punto fixo.
Dispersión radicular (distribución do servidor): distribución das lecturas do reloxo do servidor NTP en segundos cun punto fixo.
ID de ref (identificador da fonte) – ID do reloxo. Se o servidor ten o estrato 1, ref id é o nome do reloxo atómico (4 caracteres ASCII). Se o servidor usa outro servidor, entón o ref id contén o enderezo deste servidor.
Os últimos 4 campos representan o tempo - 32 bits - a parte enteira, 32 bits - a parte fraccionaria.
referencia — as últimas lecturas do reloxo no servidor.
Orixinar – hora en que se enviou o paquete (cuberto polo servidor; máis información a continuación).
Recibir – hora en que o servidor recibiu o paquete.
Transmitir – tempo de envío do paquete desde o servidor ao cliente (cuberto polo cliente, máis sobre isto a continuación).

Non consideraremos os dous últimos campos.

Escribamos o noso paquete:

Código do 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 (e recibir) un paquete ao servidor, debemos ser capaces de convertelo nunha matriz de bytes.
Para esta operación (e inversa), escribiremos dúas funcións: pack() e unpack():

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 desempaquetado

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 os preguiceiros, como unha aplicación - código que converte un paquete nunha fermosa cadea

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)

Enviando un paquete ao servidor

Debe enviarse un paquete cos campos cubertos ao servidor versión, modo и Transmitir. En Transmitir debes especificar a hora actual na máquina local (o número de segundos desde o 1 de xaneiro de 1900), versión - calquera de 1-4, modo - 3 (modo cliente).

O servidor, unha vez aceptada a solicitude, enche todos os campos do paquete NTP, copiando no campo Orixinar valor de Transmitir, que chegou na solicitude. É un misterio para min por que o cliente non pode cubrir inmediatamente o valor do seu tempo no campo Orixinar. Como resultado, cando o paquete volve, o cliente ten 4 valores de tempo: o momento en que se enviou a solicitude (Orixinar), hora en que o servidor recibiu a solicitude (Recibir), hora en que o servidor enviou a resposta (Transmitir) e a hora en que o cliente recibiu a resposta: chegar (non no paquete). Usando estes valores podemos establecer a hora correcta.

Código de envío e 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)

Procesando datos do servidor

O procesamento de datos do servidor é semellante ás accións do cabaleiro inglés do vello problema de Raymond M. Smullyan (1978): “Un home non tiña un reloxo de pulso, pero na casa había un reloxo de parede preciso, que ás veces esquecía. vento. Un día, esquecendo voltar a darlle volta ao reloxo, foi visitar o seu amigo, pasou a noite con el e, ao volver a casa, conseguiu poñer o reloxo correctamente. Como conseguiu facelo se non se sabía de antemán o tempo de viaxe? A resposta é: "Ao saír da casa, unha persoa dálle cuerda ao reloxo e lembra en que posición están as agullas. Chegado a un amigo e deixando os hóspedes, anota a hora da súa chegada e saída. Isto permítelle saber canto tempo estivo de visita. Volvendo a casa e mirando o reloxo, unha persoa determina a duración da súa ausencia. Ao restar a este tempo o tempo que pasou visitando, unha persoa descobre o tempo que pasou viaxando ida e volta. Ao engadir a metade do tempo que pasa na estrada ao tempo de deixar os hóspedes, ten a oportunidade de descubrir a hora de chegada a casa e axustar as agullas do seu reloxo en consecuencia.

Busca a hora en que o servidor está a traballar nunha solicitude:

  1. Busca o tempo de viaxe do paquete desde o cliente ata o servidor: ((Chegar – Orixinar) – (Transmitir – Recibir)) / 2
  2. Atopa a diferenza entre o tempo do cliente e do servidor:
    Recibir - Orixinar - ((Chegar - Orixinar) - (Transmitir - Recibir)) / 2 =
    2 * Recibir – 2 * Orixinar – Chegar + Orixinar + Transmitir – Recibir =
    Recibir - Orixinar - Chegar + Transmitir

Engadimos o valor resultante á hora local e gozamos da vida.

Saída do resultado

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

Fonte: www.habr.com

Engadir un comentario