Bonjour, utilisateurs de Habr. Aujourd'hui, je vais vous parler de la façon de créer votre propre client NTP simple. Principalement, nous discuterons de la structure du paquet et de la manière de traiter la réponse du serveur NTP. Le code sera écrit en Python, car, à mon avis, il n'y a pas de meilleur langage pour ce genre de choses. Les experts remarqueront la similarité du code avec celui de ntplib — je me suis justement 'inspiré' de celui-ci.
Alors, qu'est-ce que NTP ? NTP est un protocole d'interaction avec des serveurs de temps exact. Ce protocole est utilisé dans de nombreuses machines modernes. Par exemple, le service w32tm sous Windows.
Il existe au total 5 versions du protocole NTP. La première, la version 0 (1985, RFC958), est actuellement considérée comme obsolète. Actuellement, les versions plus récentes, 1 (1988, RFC1059), 2 (1989, RFC1119), 3 (1992, RFC1305) et 4 (1996, RFC2030), sont utilisées. Les versions 1 à 4 sont compatibles entre elles, elles diffèrent seulement par les algorithmes de fonctionnement. serveurs.
Format du paquet

Indicateur de saut (indicateur de correction) — un nombre indiquant un avertissement concernant la seconde de coordination. Valeur :
- 0 – pas de correction
- 1 – la dernière minute du jour contient 61 secondes
- 2 – la dernière minute du jour contient 59 secondes
- 3 – défaillance du serveur (temps non synchronisé)
Numéro de version (numéro de version) – numéro de version du protocole NTP (1-4).
Mode (mode) — mode de fonctionnement de l'expéditeur du paquet. Valeur de 0 à 7, les plus courants :
- 3 – client
- 4 – serveur
- 5 – mode broadcast
Stratum (niveau de superposition) – nombre de couches intermédiaires entre le serveur et les horloges de référence (1 – le serveur obtient les données directement des horloges de référence, 2 – le serveur obtient les données d'un serveur de niveau 1, etc.).
Pool — nombre entier signé, représentant l'intervalle maximum entre les messages successifs. Le client NTP indique ici l'intervalle auquel il prévoit d'interroger le serveur, tandis que le serveur NTP indique l'intervalle auquel il prévoit d'être interrogé. La valeur est égale au logarithme binaire des secondes.
Précision (précision) — nombre entier signé, représentant la précision des horloges système. La valeur est égale au logarithme binaire des secondes.
Délai racine (délai du serveur) – temps nécessaire aux informations d'horloge pour atteindre le serveur NTP, exprimé en nombre de secondes avec point fixe.
Dispersion racine (écart des mesures du serveur) — écart des mesures des horloges du serveur NTP, exprimé en nombre de secondes avec point fixe.
ID de référence (identifiant de la source) – id des horloges. Si le serveur a un stratum 1, alors l'id de référence – est le nom de l'horloge atomique (4 caractères ASCII). Si le serveur utilise un autre serveur, alors l'adresse de ce serveur est enregistrée dans l'id de référence.
Les 4 derniers champs représentent le temps – 32 bits – partie entière, 32 bits – partie fractionnaire.
Référence — les dernières lectures de l'horloge sur le serveur.
Origine – le moment où le paquet a été envoyé (rempli par le serveur – plus d'informations ci-dessous).
Réception – le moment où le paquet a été reçu par le serveur.
Transmission – le moment de l'envoi du paquet du serveur au client (rempli par le client, plus d'informations ci-dessous).
Nous ne considérerons pas les deux derniers champs.
Rédigeons notre paquet :
Code du paquet
class NTPPacket:
_FORMAT = "!B B b b 11I"
def __init__(self, version_number=2, mode=3, transmit=0):
# Nécessaire d'entrer la seconde intercalaire (2 bits)
self.leap_indicator = 0
# Version du protocole (3 bits)
self.version_number = version_number
# Mode de l'expéditeur (3 bits)
self.mode = mode
# Le niveau de "couche" de lecture du temps (1 octet)
self.stratum = 0
# Intervalle entre les demandes (1 octet)
self.pool = 0
# Précision (log2) (1 octet)
self.precision = 0
# Intervalle pour atteindre le serveur NTP (4 octets)
self.root_delay = 0
# Dispersion de l'horloge du serveur NTP (4 octets)
self.root_dispersion = 0
# Indicateur des horloges (4 octets)
self.ref_id = 0
# Dernière heure de mise à jour sur le serveur (8 octets)
self.reference = 0
# Temps d'envoi du paquet depuis la machine locale (8 octets)
self.originate = 0
# Temps de réception sur le serveur (8 octets)
self.receive = 0
# Temps d'envoi de la réponse depuis le serveur (8 octets)
self.transmit = transmit
Pour envoyer (et recevoir) un paquet au serveur, nous devons être capables de le transformer en un tableau d'octets.
Pour cette opération (et l'opération inverse), nous allons écrire deux fonctions – pack() et unpack():
Fonction 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))
Fonction 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
Pour les paresseux, voici une application – un code qui transforme le paquet en une belle chaîne
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)
Envoi du paquet au serveur
Un paquet avec les champs remplis doit être envoyé au serveur Version, Mode et Transmission. Il y a Transmission Il est nécessaire d'indiquer l'heure actuelle sur la machine locale (nombre de secondes depuis le 1er janvier 1900), la version - n'importe laquelle de 1 à 4, le mode - 3 (mode client).
Le serveur, après avoir reçu la demande, remplit tous les champs dans le paquet NTP, copiant les valeurs de Origine provenant de Transmission, reçu dans la demande. Je me demande pourquoi le client ne peut pas remplir directement la valeur de son heure dans le champ Origine. Au final, lorsque le paquet revient, le client a 4 valeurs de temps – le temps d'envoi de la requête (Origine), le temps de réception de la requête par le serveur (Réception), le temps d'envoi de la réponse par le serveur (Transmission) et le temps de réception de la réponse par le client – Arriver (pas dans le paquet). Avec ces valeurs, nous pouvons établir l'heure correcte.
Code d'envoi et de réception du paquet
# 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)
Traitement des données provenant du serveur
Le traitement des données du serveur est similaire aux actions d'un gentleman anglais dans l'ancienne tâche de Raymond M. Smullyan (1978) : « Un homme n'avait pas de montre-bracelet, mais avait des horloges murales précises à la maison qu'il oubliait parfois de remonter. Un jour, après avoir oublié de remonter ses horloges, il se rendit chez un ami, passa la soirée chez lui, et, en rentrant chez lui, réussit à remettre ses horloges à l'heure. Comment a-t-il réussi à le faire alors que le temps de trajet n'était pas connu à l'avance ? » La réponse est la suivante : « En sortant de chez lui, l'homme remonte ses horloges et mémorise la position des aiguilles. En arrivant chez son ami et en partant, il note l'heure de son arrivée et de son départ. Cela lui permet de savoir combien de temps il a passé chez lui. De retour chez lui et en regardant l'horloge, il détermine la durée de son absence. En soustrayant le temps qu'il a passé chez son ami de ce temps, il obtient le temps consacré au trajet aller-retour. En ajoutant à l'heure de départ une demi-heure du trajet, il peut ainsi connaître l'heure de son retour et ajuster correctement ses horloges. »
Calculons le temps que le serveur met à traiter la requête :
- Calculons le temps de trajet du paquet du client au serveur : ((Arriver - Origine) - (Transmettre - Recevoir)) / 2
- Calculons la différence entre l'heure du client et celle du serveur :
Recevoir - Origine - ((Arriver - Origine) - (Transmettre - Recevoir)) / 2 =
2 * Recevoir - 2 * Origine - Arriver + Origine + Transmettre - Recevoir =
Recevoir - Origine - Arriver + Transmettre
Ajoutons la valeur obtenue à l'heure locale et profitons de la vie.
Afficher le résultat
time_different = answer.get_time_different(arrive_time)
result = "Différence de temps : {}\nHeure du serveur : {}\n{}".format(
time_different,
datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
answer.to_display())
print(result)
Utile .
Source : habr.com
