Écrire un simple client NTP

Bonjour les habrausers. Aujourd'hui, je veux parler de la façon d'écrire votre propre client NTP simple. Fondamentalement, la conversation portera sur la structure du paquet et sur la manière dont la réponse du serveur NTP est traitée. Le code sera écrit en python car, à mon avis, il n'y a tout simplement pas de meilleur langage pour de telles choses. Les connaisseurs feront attention à la similitude du code avec le code ntplib - il m'a "inspiré".

Alors, qu’est-ce que le NTP ? NTP est un protocole de communication avec les serveurs de temps. 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. Les plus récents sont actuellement utilisés, 1er (1988, RFC1059), 2e (1989, RFC1119), 3e (1992, RFC1305) et 4e (1996, RFC2030). Les versions 1 à 4 sont compatibles entre elles, elles ne diffèrent que par les algorithmes des serveurs.

Format de paquet

Écrire un simple client NTP

Indicateur de saut (indicateur de correction) est un nombre qui indique l’avertissement de seconde intercalaire. Signification:

  • 0 - aucune correction
  • 1 - la dernière minute de la journée contient 61 secondes
  • 2 - la dernière minute de la journée contient 59 secondes
  • 3 - panne du serveur (délai de désynchronisation)

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, la plus courante :

  • 3 - client
  • 4 - serveur
  • 5 - mode diffusion

strate (niveau de superposition) - le nombre de couches intermédiaires entre le serveur et l'horloge de référence (1 - le serveur récupère les données directement de l'horloge de référence, 2 - le serveur récupère les données du serveur de niveau 1, etc.).
Piscine est un entier signé représentant l'intervalle maximum entre des messages consécutifs. Le client NTP spécifie ici l'intervalle auquel il s'attend à interroger le serveur, et le serveur NTP spécifie l'intervalle auquel il s'attend à être interrogé. La valeur est égale au logarithme binaire des secondes.
La précision (précision) est un entier signé représentant la précision de l'horloge système. La valeur est égale au logarithme binaire des secondes.
retard racine (délai du serveur) – le temps nécessaire aux lectures de l'horloge pour atteindre le serveur NTP, sous forme de nombre de secondes à virgule fixe.
dispersion des racines (dispersion du serveur) - La dispersion de l'horloge du serveur NTP sous forme de nombre de secondes à virgule fixe.
Identifiant de référence (identifiant de la source) – identifiant de la montre. Si le serveur a la strate 1, alors ref id est le nom de l'horloge atomique (4 caractères ASCII). Si le serveur utilise un autre serveur, alors le ref id contient l'adresse de ce serveur.
Les 4 derniers champs sont l'heure - 32 bits - la partie entière, 32 bits - la partie fractionnaire.
Référence - la dernière horloge du serveur.
Origine – l'heure à laquelle le paquet a été envoyé (remplie par le serveur – plus d'informations ci-dessous).
Obtenez – heure à laquelle le paquet a été reçu par le serveur.
Transmettre – heure à laquelle le paquet a été envoyé du serveur au client (rempli par le client, plus d'informations ci-dessous).

Les deux derniers champs ne seront pas pris en compte.

Écrivons notre package :

Code du forfait

class NTPPacket:
    _FORMAT = "!B B b b 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

Pour envoyer (et recevoir) un paquet au serveur, il faut pouvoir le transformer en un tableau d'octets.
Pour cette opération (et inverse), nous écrirons 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 de décompression

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, en tant qu'application - code qui transforme le package 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 d'un package au serveur

Envoyer un paquet avec les champs remplis au serveur Version, Mode и Transmettre. la Transmettre vous devez spécifier l'heure actuelle sur la machine locale (nombre de secondes depuis le 1er janvier 1900), version - l'une des 1 à 4, mode - 3 (mode client).

Le serveur, ayant reçu la demande, remplit tous les champs du paquet NTP, en copiant dans le champ Origine valeur de Transmettre, qui est venu dans la demande. C'est un mystère pour moi pourquoi le client ne peut pas immédiatement évaluer la valeur de son temps sur le terrain. Origine. En conséquence, lorsque le paquet revient, le client dispose de 4 valeurs temporelles - l'heure à laquelle la demande a été envoyée (Origine), l'heure à laquelle le serveur a reçu la demande (Obtenez ), l'heure à laquelle le serveur a envoyé la réponse (Transmettre) et l'heure de réception d'une réponse par le client - Arrivée (pas dans le colis). Avec ces valeurs, nous pouvons régler l'heure correcte.

Code d'envoi et de réception du colis

# 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 depuis le serveur

Le traitement des données du serveur est similaire aux actions du gentleman anglais du vieux problème de Raymond M. Smallian (1978) : « Une personne n'avait pas de montre-bracelet, mais il y avait une horloge murale précise à la maison, qu'il parfois j'ai oublié de remonter. Un jour, oubliant de remettre l'horloge en marche, il alla rendre visite à son ami, passa la soirée avec lui et, de retour chez lui, il réussit à régler l'horloge correctement. Comment y est-il parvenu si la durée du trajet n’était pas connue à l’avance ? La réponse est : « En quittant la maison, une personne remonte l’horloge et se souvient de la position des aiguilles. En venant chez un ami et en quittant les invités, il note l'heure de son arrivée et de son départ. Cela lui permet de savoir combien de temps il a été absent. De retour chez elle et en regardant l'horloge, une personne détermine la durée de son absence. En soustrayant de ce temps le temps qu'elle a passé en visite, la personne découvre le temps passé sur la route aller et retour. En ajoutant la moitié du temps passé sur la route à l'heure du départ des invités, il a la possibilité de connaître l'heure d'arrivée chez lui et d'ajuster les aiguilles de son horloge en conséquence.

Recherchez l'heure à laquelle le serveur travaillait sur la requête :

  1. Recherche du temps de trajet des paquets du client au serveur : ((Arrivée - Origine) - (Transmettre - Recevoir)) / 2
  2. Trouvez la différence entre l'heure du client et celle du serveur :
    Réception - Origine - ((Arrivée - Origine) - (Transmission - Réception)) / 2 =
    2 * Recevoir - 2 * Origine - Arriver + Origine + Transmettre - Recevoir =
    Recevoir - Originer - Arriver + Transmettre

Nous ajoutons la valeur reçue à l'heure locale et profitons de la vie.

Sortie du résultat

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)

Utile lien.

Source: habr.com

Ajouter un commentaire