Escrevendo um cliente NTP simples

Olá, habrayuzery. Hoje eu quero falar sobre como escrever meu cliente NTP simples. Basicamente, falaremos sobre a estrutura do pacote e como processar a resposta do servidor NTP. O código será escrito em python, porque, ao que me parece, a melhor linguagem para essas coisas simplesmente não pode ser encontrada. Os especialistas prestarão atenção à semelhança do código com o código ntplib - eu fui "inspirado" por ele.

Então, o que exatamente é o NTP? NTP - protocolo de 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, os mais novos são usados, 1º (1988, RFC1059), 2º (1989, RFC1119), 3º (1992, RFC1305) e 4º (1996, RFC2030). 1 a 4 versões são compatíveis entre si, diferem apenas nos algoritmos de operação do servidor.

Formato do pacote




Indicador de salto ( indicador de correção) - um número mostrando um aviso sobre a segunda 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 - mau funcionamento do servidor (horário não sincronizado)

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

Modo - modo de operação do remetente de pacote. Valor de 0 a 7, o mais comum:

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

Estrato (nível de estratificação) - 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 do servidor com nível 1, etc.).
Poll é um número inteiro assinado que representa o intervalo máximo entre mensagens consecutivas. Aqui, o cliente NTP indica o intervalo no qual espera pesquisar o servidor, e o servidor NTP indica o intervalo no qual espera ser pesquisado. O valor é o logaritmo binário de segundos.
Precisão é um número inteiro assinado que representa a precisão do relógio do sistema. O valor é o logaritmo binário de segundos.
Atraso na raiz ( atraso do servidor) - o tempo durante o qual o relógio atinge o servidor NTP, como o número de segundos com um ponto fixo.
Dispersão raiz - a dispersão do relógio do servidor NTP como o número de segundos com um ponto fixo.
ID de referência (identificador de origem) - identificação do relógio. Se o servidor tiver um estrato 1, ref id será o nome do relógio atômico (4 caracteres ASCII). Se o servidor usar outro servidor, o endereço desse servidor será gravado no ID de referência.
Os últimos 4 campos são tempo - 32 bits - a parte inteira, 32 bits - parte fracionária.
Referência - o relógio mais recente no servidor.
Origem - a hora em que o pacote foi enviado (preenchido pelo servidor - mais sobre isso abaixo).
Receber - hora em que o pacote foi recebido pelo servidor.
Transmitir - tempo em que o pacote foi enviado 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 = "!BB bb 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 para o servidor, devemos poder transformá-lo em uma matriz de bytes.
Para esta operação (e reversa), 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)) 


Para selecionar a parte fracionária do número para gravar no pacote, precisamos da função get_fraction ():
get_fraction ()
 def get_fraction(number, precision): return int((number - int(number)) * 2 ** precision) 


Função desembalar
 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 pessoas preguiçosas, como um aplicativo - um código que transforma um pacote em uma bela 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


É necessário enviar um pacote ao servidor com os campos Versão , Modo e Transmissão preenchidos. Em Transmit, você precisa especificar o horário atual na máquina local (número de segundos desde 1º de janeiro de 1900), versão - qualquer um dos 1-4, modo - 3 (modo cliente).

Após aceitar a solicitação, o servidor preenche todos os campos no pacote NTP, copiando o valor Transmitir da solicitação para o campo Originate . É um mistério para mim o motivo pelo qual o cliente não pode preencher imediatamente o valor de seu tempo no campo Origem . Como resultado, quando o pacote volta, o cliente tem quatro vezes - a hora em que a solicitação foi enviada ( Origem ), a hora em que o servidor recebeu a solicitação ( Receber ), a 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 da antiga tarefa de Raymond M. Sullian (1978): “Uma pessoa não tinha relógio, mas, por outro lado, havia um relógio de parede exato que às vezes esquecia de iniciar. Uma vez, esquecido de reiniciar o relógio, foi visitar o amigo, passou a noite naquele local e, quando voltou para casa, conseguiu acertar o relógio corretamente. Como ele conseguiu fazer isso se o tempo da viagem não era conhecido com antecedência? ” A resposta é: “Saindo de casa, uma pessoa liga o relógio e lembra em que posição os ponteiros estão. Chegando a um amigo e deixando os convidados, ele anota a hora de sua chegada e partida. Isso permite que ele descubra quanto estava visitando. Depois de voltar para casa e olhar para o relógio, uma pessoa determina a duração de sua ausência. Subtraindo desse tempo o tempo que ele passou em uma visita, uma pessoa aprende o tempo gasto na viagem de um lado para outro. Depois de adicionar metade do tempo gasto na viagem ao horário de saída dos convidados, ele tem a oportunidade de descobrir a hora de chegada em casa e traduzir os ponteiros do relógio de acordo.

Encontramos o tempo de trabalho do servidor na solicitação:

  1. Nós encontramos o tempo do caminho do pacote do cliente para o servidor: ((Chegada - Origem) - (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

Adicione o valor obtido à hora local e aproveite a vida.

Resultado de saída
 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) 


Link útil.

Source: https://habr.com/ru/post/pt448060/


All Articles