Estamos escrevendo um cliente NTP simples

Olá, usuários do Habr. Hoje, quero discutir como escrever um cliente NTP simples. Principalmente, vamos falar sobre a estrutura do pacote e a forma de processar a resposta do servidor NTP. O código será escrito em Python, pois, na minha opinião, não existe linguagem melhor para esse tipo de tarefa. Os especialistas notarão a semelhança do código com o código do ntplib — eu me “inspirei” nele.

Então, o que é exatamente o NTP? NTP é um protocolo de interação com servidores de tempo preciso. Este protocolo é utilizado em muitas máquinas modernas. Por exemplo, o serviço w32tm no Windows.

Existem, ao todo, 5 versões do protocolo NTP. A primeira, versão 0 (1985, RFC958), é atualmente considerada obsoleta. As versões mais novas são a 1 (1988, RFC1059), 2 (1989, RFC1119), 3 (1992, RFC1305) e 4 (1996, RFC2030). As versões 1-4 são compatíveis entre si, diferenciando-se apenas pelos algoritmos de funcionamento. servidores.

Formato do pacote

Estamos escrevendo um cliente NTP simples

Indicador de leap (indicador de correção) — número que mostra o aviso sobre a segunda de coordenação. Valor:

  • 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 – falha do servidor (tempo não está sincronizado)

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

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

  • 3 – cliente
  • 4 – servidor
  • 5 – modo de broadcast

Estrato (nível de estrato) – número de camadas intermediárias entre o servidor e os relógios de referência (1 – o servidor obtém dados diretamente dos relógios de referência, 2 – o servidor obtém dados de um servidor de nível 1, etc.).
Pool — número inteiro com sinal, representando o intervalo máximo entre mensagens consecutivas. O cliente NTP indica aqui o intervalo com o qual pretende consultar o servidor, e o servidor NTP – o intervalo com o qual deseja ser consultado. O valor corresponde ao logaritmo binário em segundos.
Precisão (precisão) — número inteiro com sinal, representando a precisão dos relógios do sistema. O valor corresponde ao logaritmo binário em segundos.
Atraso raiz (atraso do servidor) – o tempo que os dados do relógio levam para chegar ao servidor NTP, expresso em segundos com ponto fixo.
Dispersão raiz (dispersão de leituras do servidor) — variação das leituras do relógio do servidor NTP, expresso em segundos com ponto fixo.
ID de referência (identificador da fonte) – id dos relógios. Se o servidor tiver stratum 1, então ref id – o nome dos relógios atômicos (4 caracteres ASCII). Se o servidor usar outro servidor, o endereço desse servidor será gravado em ref id.
Os últimos 4 campos representam o tempo – 32 bits – parte inteira, 32 bits – parte fracionária.
Referência — as últimas leituras do relógio no servidor.
Origem – o tempo em que o pacote foi enviado (preenchido pelo servidor – sobre isso abaixo).
Receber – o tempo em que o pacote foi recebido pelo servidor.
Transmitir – o tempo de envio do pacote do servidor para o cliente (preenchido pelo cliente, sobre isso também abaixo).

Não vamos considerar 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):
        # Necessário para inserir segundo bissexto (2 bits)
        self.leap_indicator = 0
        # Versão do protocolo (3 bits)
        self.version_number = version_number
        # Modo do remetente (3 bits)
        self.mode = mode
        # O nível de "camadas" da leitura do tempo (1 byte)
        self.stratum = 0
        # Intervalo entre requisições (1 byte)
        self.pool = 0
        # Precisão (log2) (1 byte)
        self.precision = 0
        # Intervalo para o relógio alcançar o servidor NTP (4 bytes)
        self.root_delay = 0
        # Dispersão do relógio do servidor NTP (4 bytes)
        self.root_dispersion = 0
        # Indicador dos relógios (4 bytes)
        self.ref_id = 0
        # Última hora de atualização no servidor (8 bytes)
        self.reference = 0
        # Hora de envio do pacote da máquina local (8 bytes)
        self.originate = 0
        # Hora de recebimento no servidor (8 bytes)
        self.receive = 0
        # Hora de envio da resposta do servidor (8 bytes)
        self.transmit = transmit

Para enviar (e receber) um pacote no servidor, precisamos saber como convertê-lo em um array de bytes.
Para essa (e a operação inversa) escreveremos duas funções — pack() e unpack():

Função pack

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 unpack

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 preguiçosos, como anexo – um código que transforma o pacote em uma string bonita

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)

Envio de pacote para o servidor

Precisamos enviar um pacote ao servidor com os campos preenchidos Versão, Modo e Transmitir. No Transmitir é necessário especificar a hora atual na máquina local (número de segundos desde 1 de janeiro de 1900), a versão – qualquer uma de 1 a 4, o modo – 3 (modo cliente).

O servidor, ao receber a solicitação, preenche todos os campos no pacote NTP, copiando para o campo Origem o valor de Transmitir, que chegou na solicitação. Para mim, é um mistério por que o cliente não pode preencher imediatamente o valor do seu tempo no campo Origem. No final, quando o pacote retorna, o cliente tem 4 valores de tempo – o tempo de envio da solicitação (Origem), o tempo de recepção da solicitação pelo servidor (Receber), o tempo de envio da resposta pelo servidor (Transmitir) e o tempo de recepção da resposta pelo cliente – Chegar não está no pacote). Com esses valores, podemos estabelecer o horário correto.

Código para envio e recepção do pacote

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

Processamento de dados do servidor

O processamento de dados do servidor é semelhante às ações de um cavalheiro inglês de uma antiga tarefa de Raymond M. Smullyan (1978): "Um homem não tinha um relógio de pulso, mas em casa havia um relógio de parede preciso, que ele às vezes esquecia de ajustar. Uma vez, esquecendo-se de ajustar o relógio novamente, ele foi visitar um amigo, passou a noite lá e, ao voltar para casa, conseguiu ajustar o relógio corretamente. Como ele fez isso, se o tempo de viagem não era conhecido de antemão?" A resposta é: "Ao sair de casa, o homem ajusta o relógio e se lembra da posição dos ponteiros. Ao chegar na casa do amigo e ao sair de lá, ele anota o horário de chegada e de saída. Isso permite que ele saiba quanto tempo ficou visitando. Ao voltar para casa e olhar para o relógio, o homem determina a duração de sua ausência. Subtraindo desse tempo o tempo que ele passou visitando, ele descobre o tempo gasto na viagem de ida e volta. Ao somar à hora de saída da visita metade do tempo gasto na viagem, ele consegue saber que horas ele chega em casa e ajustar os ponteiros do seu relógio de acordo."

Calculamos o tempo que o servidor leva para processar a solicitação:

  1. Calculamos o tempo de percurso do pacote do cliente para o servidor: ((Chegar – Origem) — (Transmitir – Receber)) / 2
  2. Calculamos a diferença entre o horário do cliente e do servidor:
    Receber — Origem — ((Chegar – Origem) — (Transmitir – Receber)) / 2 =
    2 * Receber — 2 * Origem – Chegar + Origem + Transmitir – Receber =
    Receber – Origem – Chegar + Transmitir

Adicionamos o valor obtido ao horário local e celebramos a vida.

Exibindo o resultado

time_different = answer.get_time_different(arrive_time)
result = "Diferença de tempo: {}nHora do servidor: {}n{}".format(
    time_different,
    datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
    answer.to_display())
print(result)

Útil link.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster