L’autre jour, mon ordinateur professionnel a passé quelques dizaines de minutes avec son horloge décalée d’un peu moins d’une heure, alors que pourtant il se synchronise régulièrement avec un serveur quelque part sur le réseau.
Cela a éveillé ma curiosité sur comment tout cela fonctionne !
NTP, un protocole pour connaître l'heure
Sur les réseaux informatiques, les machines synchronisent leurs horloges en s’échangeant des messages selon le protocole NTP (Network Time Protocol).
C’est un protocole relativement complexe, qui explique comment les différentes machines doivent communiquer entre elles et synchroniser leurs horloges pour que tout se passe bien et que tout le monde soit bien à la même heure. La RFC 5905 en propose une description complète. Quand on ne programme pas un serveur de temps un peu costaud, on peut se contenter de la version simplifiée, décrite dans la RFC 4330.
Le protocole définit une hiérarchie de serveurs de qualité décroissante :
- la strate 1, des serveurs qui obtiennent leur heure directement d’une horloge de référence (comme le GPS ou Galileo) ;
- la strate 2, des serveurs qui obtiennent leur heure de serveurs de la strate 1,
- etc.
Les serveurs de strate 1 ne devraient pas être interrogés directement par le commun des mortels. Donc, on reste poli et on interroge seulement des serveurs de strate 2 ou plus. Mais on peut faire encore mieux que de spammer un serveur en particulier, et faire appel à un pool de serveur, pour peu qu’on ne soit pas trop exigeant sur la précision.
Le protocole utilise UDP sur le port 123.
Dans son plus simple appareil, demander l’heure est très simple :
- on envoie une requête au serveur de son choix ;
- on attend sa réponse qui contient un timestamp.
Il suffit alors de faire quelques calculs pour connaître l’heure UTC.
Si on est courageux, on peut gérer un peu plus de complexité pour obtenir une heure plus précise, mais je ne le suis pas.
Python, va demander l'heure au monsieur
En lisant les RFC, on voit qu’on peut se contenter d’envoyer un paquet avec vraiment très peu d’information. Parmi tous les champs NTP, il suffit en effet d’indiquer :
- la version du protocole (4 dans notre cas) ;
- le mode (3 dans notre cas, ce qui veut dire qu’on est un client).
On peut laisser le reste à zéro.
En retour, le serveur nous répond plein de choses, dont la seule qui m’intéresse réellement est le timestamp (et encore seulement les secondes). Il s’agit du nombre de secondes depuis le 1er janvier 1900 qu’on convertit aisément en date et qu’on affiche.
J’ai fait un script Python (un peu sale) qui fait le job !
import socket
import datetime
# NTP server
host = "pool.ntp.org"
# NTP port
port = 123
# Length of the NTP header sent or received (bytes)
header_length = 48
# Offset of the NTP timestamp field (bytes)
timestamp_offset = 40
# Origin of the NTP timestamp
dt = datetime.datetime(1900, 1, 1)
# Query the NTP server
s = socket.socket(type=socket.SOCK_DGRAM)
request = b"\x23" + b"\x00" * (header_length - 1)
s.sendto(request, (host, port))
d, _ = s.recvfrom(header_length)
# Compute and display the date
ts = int(d[timestamp_offset]) * 256 ** 3 + int(d[timestamp_offset + 1]) * 256 ** 2 + int(d[timestamp_offset + 2]) * 256 ** 1 + int(d[timestamp_offset + 3])
now = dt + datetime.timedelta(seconds=ts)
print(f"{now}")
Il s’utilise comme cela :
$ python3 ntp.py
2021-03-14 21:34:14