Escrevendo um cliente NTP simples

Olá, Habrausers. Hoje quero falar sobre como escrever seu próprio cliente NTP simples. Basicamente, a conversa se voltará para a estrutura do pacote e o método de processamento da resposta do servidor NTP. O código será escrito em Python, porque me parece que simplesmente não existe uma linguagem melhor para essas coisas. Os conhecedores notarão a semelhança do código com o código ntplib - fui “inspirado” por ele.

Então, o que exatamente é NTP? NTP é um protocolo para interação com servidores de horário exato. Este protocolo é usado em muitas máquinas modernas. Por exemplo, o serviço w32tm no Windows.

Existem 5 versões do protocolo NTP no total. A primeira, versão 0 (1985, RFC958)), é atualmente considerada obsoleta. Agora são usados ​​os mais novos, 1º (1988, RFC1059), 2º (1989, RFC1119), 3º (1992, RFC1305) e 4º (1996, RFC2030). As versões 1 a 4 são compatíveis entre si; elas diferem apenas nos algoritmos de operação do servidor.

Formato do pacote

Escrevendo um cliente NTP simples

Indicador de salto (indicador de correção) - um número que indica um aviso sobre o segundo de coordenação. Significado:

  • 0 – sem correção
  • 1 – o último minuto do dia contém 61 segundos
  • 2 – o último minuto do dia contém 59 segundos
  • 3 – mau funcionamento do servidor (a hora não está sincronizada)

Número da versão (número da versão) – número da versão do protocolo NTP (1-4).

Moda (modo) — modo de operação do remetente do pacote. Valor de 0 a 7, mais comum:

  • 3 – cliente
  • 4 – servidor
  • 5 – modo de transmissão

Estrato (nível de camada) – o número de camadas intermediárias entre o servidor e o relógio de referência (1 – o servidor obtém dados diretamente do relógio de referência, 2 – o servidor obtém dados de um servidor com camada 1, etc.).
Piscina é um número inteiro assinado que representa o intervalo máximo entre mensagens consecutivas. O cliente NTP especifica aqui o intervalo em que espera sondar o servidor, e o servidor NTP especifica o intervalo em que espera ser sondado. O valor é igual ao logaritmo binário de segundos.
Precisão (precisão) é um número inteiro assinado que representa a precisão do relógio do sistema. O valor é igual ao logaritmo binário de segundos.
Atraso raiz (atraso do servidor) – o tempo que leva para as leituras do relógio chegarem ao servidor NTP, como um número fixo de segundos.
Dispersão de raiz (propagação do servidor) - propagação das leituras do relógio do servidor NTP como um número de segundos com um ponto fixo.
ID de referência (identificador de origem) – id do relógio. Se o servidor tiver estrato 1, então ref id é o nome do relógio atômico (4 caracteres ASCII). Se o servidor usar outro servidor, o ref id conterá o endereço desse servidor.
Os últimos 4 campos representam o tempo - 32 bits - a parte inteira, 32 bits - a parte fracionária.
Referência — as últimas leituras do relógio no servidor.
Originar – hora em que o pacote foi enviado (preenchido pelo servidor - mais sobre isso abaixo).
Recebimento – hora em que o pacote foi recebido pelo servidor.
Transmitir – horário de envio do pacote do servidor para o cliente (preenchido pelo cliente, mais sobre isso abaixo).

Não consideraremos os dois últimos campos.

Vamos escrever nosso pacote:

Código do pacote

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 receber) um pacote ao servidor, devemos ser capazes de transformá-lo em uma matriz de bytes.
Para esta operação (e reversa), escreveremos duas funções - pack() e unpack():

função de pacote

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

função de descompactar

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 preguiçosos, como um aplicativo - código que transforma um pacote em uma linda string

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 um pacote para o servidor

Um pacote com campos preenchidos deve ser enviado ao servidor Versão, Moda и Transmitir. Em Transmitir você deve especificar a hora atual na máquina local (o número de segundos desde 1º de janeiro de 1900), versão - qualquer um de 1-4, modo - 3 (modo cliente).

O servidor, tendo aceito a solicitação, preenche todos os campos do pacote NTP, copiando para o campo Originar valor de Transmitir, que veio na solicitação. É um mistério para mim porque o cliente não consegue preencher imediatamente o valor do seu tempo no campo Originar. Como resultado, quando o pacote volta, o cliente tem 4 valores de tempo - o horário em que a solicitação foi enviada (Originar), hora em que o servidor recebeu a solicitação (Recebimento), hora em que o servidor enviou a resposta (Transmitir) e a hora em que o cliente recebeu a resposta – Chegar (não no pacote). Usando esses valores podemos definir a hora correta.

Código de envio e recebimento de pacotes

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

Processando dados do servidor

O processamento de dados do servidor é semelhante às ações do cavalheiro inglês do antigo problema de Raymond M. Smullyan (1978): “Um homem não tinha relógio de pulso, mas havia um relógio de parede preciso em casa, que ele às vezes esquecia para enrolar. Um dia, esquecendo novamente de dar corda no relógio, foi visitar o amigo, passou a noite com ele e, quando voltou para casa, conseguiu acertar o relógio. Como ele conseguiu fazer isso se o tempo de viagem não era conhecido com antecedência? A resposta é: “Ao sair de casa, a pessoa dá corda no relógio e lembra em que posição estão os ponteiros. Chegando a um amigo e deixando os convidados, ele anota a hora de sua chegada e saída. Isso permite que ele descubra há quanto tempo ele estava visitando. Voltando para casa e olhando o relógio, a pessoa determina a duração de sua ausência. Ao subtrair desse tempo o tempo que passou visitando, a pessoa descobre o tempo gasto viajando de ida e volta. Ao somar metade do tempo gasto na estrada ao horário de saída dos convidados, ele tem a oportunidade de saber a hora de chegada em casa e ajustar os ponteiros do relógio de acordo.”

Encontre o horário em que o servidor está trabalhando em uma solicitação:

  1. Encontre o tempo de viagem do pacote do cliente ao servidor: ((Chegar – Originar) – (Transmitir – Receber)) / 2
  2. Encontre a diferença entre o horário do cliente e do servidor:
    Receber - Originar - ((Chegar - Originar) - (Transmitir - Receber)) / 2 =
    2 * Receber – 2 * Originar – Chegar + Originar + Transmitir – Receber =
    Receber – Originar – Chegar + Transmitir

Adicionamos o valor resultante à hora local e aproveitamos a 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 link.

Fonte: habr.com

Adicionar um comentário