Hola, usuarios de Habr. Hoy quiero hablar sobre cómo escribir un cliente NTP sencillo. Principalmente, discutiremos la estructura del paquete y cómo procesar la respuesta del servidor NTP. El código se escribirá en Python, porque, en mi opinión, no hay mejor lenguaje para este tipo de cosas. Los expertos notarán la similitud del código con el de ntplib; de hecho, me 'inspiré' en él.
Entonces, ¿qué es exactamente NTP? NTP es un protocolo de interacción con servidores de tiempo preciso. Este protocolo se utiliza en muchas máquinas modernas. Por ejemplo, el servicio w32tm en Windows.
Existen 5 versiones del protocolo NTP. La primera, la versión 0 (1985, RFC958), actualmente se considera obsoleta. Ahora se utilizan versiones más nuevas: la 1 (1988, RFC1059), la 2 (1989, RFC1119), la 3 (1992, RFC1305) y la 4 (1996, RFC2030). Las versiones 1-4 son compatibles entre sí, diferenciándose solo por los algoritmos de funcionamiento. servidores.
Formato del paquete

Indicador de salto (indicador de corrección) — número que indica una advertencia sobre el segundo de coordinación. Valor:
- 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 no sincronizado)
Número de versión (número de versión) – número de la versión del protocolo NTP (1-4).
Modo (modo) — modo de operación del remitente del paquete. Valor de 0 a 7, los más comunes:
- 3 – cliente
- 4 – servidor
- 5 – modo de difusión
Estrato (nivel de superposición) – la cantidad de capas intermedias entre el servidor y los relojes de referencia (1 – el servidor obtiene datos directamente de los relojes de referencia, 2 – el servidor obtiene datos de un servidor de nivel 1, etc.).
Pool – número entero con signo que representa el máximo intervalo entre mensajes consecutivos. El cliente NTP especifica aquí el intervalo con el que planea consultar al servidor, y el servidor NTP – el intervalo con el que espera ser consultado. El valor es igual al logaritmo binario de los segundos.
Precisión (precisión) — número entero con signo que representa la precisión de los relojes del sistema. El valor es igual al logaritmo binario de los segundos.
Retraso raíz (retraso del servidor) – el tiempo que tardan las lecturas del reloj en llegar al servidor NTP, expresado como un número de segundos con punto fijo.
Dispersión raíz (dispersión de las lecturas del servidor) — dispersion de las lecturas del reloj del servidor NTP expresada como un número de segundos con punto fijo.
ID de referencia (identificador de origen) – id del reloj. Si el servidor tiene un estrato 1, entonces el ref id – el nombre del reloj atómico (4 caracteres ASCII). Si el servidor utiliza otro servidor, entonces en ref id se registra la dirección de ese servidor.
Los últimos 4 campos representan el tiempo – 32 bits – parte entera, 32 bits – parte fraccionaria.
Referencia — las últimas lecturas del reloj en el servidor.
Origen – el tiempo en que se envió el paquete (completado por el servidor – sobre esto más adelante).
Recepción – el tiempo en que el servidor recibe el paquete.
Transmisión – el tiempo de envío del paquete desde el servidor al cliente (completado por el cliente, sobre esto también más adelante).
No consideraremos los dos últimos campos.
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):
# Necesario para ingresar segundo bisiesto (2 bits)
self.leap_indicator = 0
# Versión del protocolo (3 bits)
self.version_number = version_number
# Modo del emisor (3 bits)
self.mode = mode
# El nivel de "capa" de tiempo de lectura (1 byte)
self.stratum = 0
# Intervalo entre solicitudes (1 byte)
self.pool = 0
# Precisión (log2) (1 byte)
self.precision = 0
# Intervalo para el reloj que llega al servidor NTP (4 bytes)
self.root_delay = 0
# Dispersión del reloj en el servidor NTP (4 bytes)
self.root_dispersion = 0
# Indicador de relojes (4 bytes)
self.ref_id = 0
# Última hora de actualización en el servidor (8 bytes)
self.reference = 0
# Hora de envío del paquete desde la máquina local (8 bytes)
self.originate = 0
# Hora de recepción en el servidor (8 bytes)
self.receive = 0
# Hora de envío de la respuesta desde el servidor (8 bytes)
self.transmit = transmit
Para enviar (y recibir) un paquete al servidor, debemos ser capaces de convertirlo en un arreglo de bytes.
Para esta (y la operación inversa) escribiremos dos funciones — pack() y unpack():
Función 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))
Función 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 los perezosos, como una adición: código que convierte un paquete en una cadena hermosa
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)
Enviar paquete al servidor
Es necesario enviar un paquete al servidor con los campos completos Versión, Modo y Transmisión. Hay Transmisión es necesario especificar la hora actual en la máquina local (cantidad de segundos desde el 1 de enero de 1900), la versión — cualquiera de 1-4, el modo — 3 (modo cliente).
El servidor, al recibir la solicitud, llena todos los campos en el paquete NTP copiando el campo Origen el valor de Transmisión, recibido en la solicitud. Me resulta un misterio por qué el cliente no puede llenar inmediatamente su tiempo en el campo Origen. Al final, cuando el paquete regresa, el cliente tiene 4 valores de tiempo: el tiempo de envío de la solicitud (Origen), el tiempo de recepción de la solicitud por el servidor (Recepción), el tiempo de envío de la respuesta por el servidor (Transmisión) y el tiempo de recepción de la respuesta por el cliente – Llegar (no en el paquete). Con estos valores podemos establecer la hora correcta.
Código para el envío y recepción del paquete
# 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 del servidor
El procesamiento de datos desde el servidor se asemeja a las acciones de un caballero inglés en el antiguo problema de Raymond M. Smullyan (1978): "Un hombre no tenía reloj de pulsera, pero en su casa había un reloj de pared exacto que a veces olvidaba dar cuerda. Un día, al olvidar de nuevo darle cuerda, fue a visitar a un amigo, pasó la tarde con él y, al regresar a casa, logró ajustar el reloj correctamente. ¿Cómo pudo hacerlo, si el tiempo de viaje no se conocía de antemano?" La respuesta es: "Al salir de casa, el hombre da cuerda al reloj y recuerda en qué posición están las manecillas. Al llegar al amigo y partir de la visita, anota la hora de llegada y salida. Esto le permite saber cuánto tiempo estuvo de visita. Al regresar a casa y mirar el reloj, el hombre determina la duración de su ausencia. Restando de este tiempo el tiempo que pasó de visita, obtiene el tiempo gastado en el trayecto de ida y vuelta. Al sumar a la hora de salida de la visita la mitad del tiempo empleado en el trayecto, puede calcular la hora de llegada a casa y ajustar las manecillas de su reloj en consecuencia."
Encontramos el tiempo que el servidor tarda en procesar la solicitud:
- Encontramos el tiempo de trayecto del paquete desde el cliente al servidor: ((Llegada – Origen) — (Transmitir – Recibir)) / 2
- Encontramos la diferencia entre el tiempo del cliente y el servidor:
Recibir — Origen — ((Llegada – Origen) — (Transmitir – Recibir)) / 2 =
2 * Recibir — 2 * Origen – Llegada + Origen + Transmitir – Recibir =
Recibir – Origen – Llegada + Transmitir
Sumamos el valor obtenido al tiempo local y disfrutamos de la vida.
Mostrar resultado
time_different = answer.get_time_different(arrive_time)
result = "Diferencia de tiempo: {}nHora del servidor: {}n{}".format(
time_different,
datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
answer.to_display())
print(result)
Útil .
Fuente: habr.com
