Scrivere un semplice client NTP

Ciao, Habrauser. Oggi voglio parlare di come scrivere il proprio semplice client NTP. Fondamentalmente la conversazione riguarderà la struttura del pacchetto e il metodo di elaborazione della risposta dal server NTP. Il codice sarà scritto in Python, perché mi sembra che semplicemente non esista un linguaggio migliore per queste cose. Gli intenditori noteranno la somiglianza del codice con il codice ntplib: ne sono stato "ispirato".

Allora, cos’è esattamente l’NTP? NTP è un protocollo per l'interazione con server dell'ora esatta. Questo protocollo è utilizzato in molte macchine moderne. Ad esempio, il servizio w32tm in windows.

Esistono 5 versioni in totale del protocollo NTP. La prima, la versione 0 (1985, RFC958)), è attualmente considerata obsoleta. Ora vengono utilizzati quelli più recenti, 1° (1988, RFC1059), 2° (1989, RFC1119), 3° (1992, RFC1305) e 4° (1996, RFC2030). Le versioni 1-4 sono compatibili tra loro; differiscono solo negli algoritmi di funzionamento del server.

Formato del pacchetto

Scrivere un semplice client NTP

Indicatore di salto (indicatore di correzione) - un numero che indica un avviso sul secondo di coordinazione. Senso:

  • 0 – nessuna correzione
  • 1 – l'ultimo minuto della giornata contiene 61 secondi
  • 2 – l'ultimo minuto della giornata contiene 59 secondi
  • 3 – malfunzionamento del server (l'ora non è sincronizzata)

Numero di versione (numero di versione) – Numero di versione del protocollo NTP (1-4).

Moda (modalità) — modalità operativa del mittente del pacchetto. Valore da 0 a 7, più comune:

  • 3 – cliente
  • 4 – servitore
  • 5 – modalità di trasmissione

falda (livello di stratificazione) – il numero di livelli intermedi tra il server e l'orologio di riferimento (1 – il server prende i dati direttamente dall'orologio di riferimento, 2 – il server prende i dati da un server con il livello 1, ecc.).
Piscina è un numero intero con segno che rappresenta l'intervallo massimo tra messaggi consecutivi. Il client NTP specifica qui l'intervallo in cui si aspetta di interrogare il server, e il server NTP specifica l'intervallo in cui si aspetta di essere interrogato. Il valore è uguale al logaritmo binario dei secondi.
Precisione (precisione) è un numero intero con segno che rappresenta la precisione dell'orologio di sistema. Il valore è uguale al logaritmo binario dei secondi.
Ritardo della radice (ritardo del server) – il tempo impiegato dalle letture dell'orologio per raggiungere il server NTP, come numero di secondi a virgola fissa.
Dispersione delle radici (diffusione del server) - diffusione delle letture dell'orologio del server NTP come numero di secondi con un punto fisso.
Codice rif (identificatore della fonte) – id orologio. Se il server ha lo strato 1, ref id è il nome dell'orologio atomico (4 caratteri ASCII). Se il server utilizza un altro server, il ref id contiene l'indirizzo di questo server.
Gli ultimi 4 campi rappresentano l'ora - 32 bit - la parte intera, 32 bit - la parte frazionaria.
Riferimento — le ultime letture dell'orologio sul server.
origine – ora in cui il pacchetto è stato inviato (compilata dal server - ne parleremo più avanti).
Ricevere – ora in cui il pacchetto è stato ricevuto dal server.
Trasmettere – ora di invio del pacchetto dal server al client (compilato dal client, ne parleremo più avanti).

Non prenderemo in considerazione gli ultimi due campi.

Scriviamo il nostro pacchetto:

Codice del pacchetto

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

Per inviare (e ricevere) un pacchetto al server, dobbiamo essere in grado di trasformarlo in un array di byte.
Per questa operazione (e inversa), scriveremo due funzioni: pack() e unpack():

funzione di confezione

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))

funzione di disimballaggio

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

Per i pigri, come un codice applicativo che trasforma un pacchetto in una bellissima stringa

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)

Invio di un pacchetto al server

Un pacchetto con i campi compilati deve essere inviato al server Versione, Moda и Trasmettere. In Trasmettere è necessario specificare l'ora corrente sul computer locale (il numero di secondi dal 1 gennaio 1900), versione - qualsiasi tra 1-4, modalità - 3 (modalità client).

Il server, accettata la richiesta, compila tutti i campi del pacchetto NTP, copiandoli nel campo origine valore da Trasmettere, contenuto nella richiesta. Per me è un mistero il motivo per cui il cliente non possa immediatamente valorizzare il suo tempo sul campo origine. Di conseguenza, quando il pacchetto ritorna, il client ha 4 valori temporali: l'ora in cui è stata inviata la richiesta (origine), l'ora in cui il server ha ricevuto la richiesta (Ricevere), l'ora in cui il server ha inviato la risposta (Trasmettere) e l'ora in cui il cliente ha ricevuto la risposta – Arrivare (non nella confezione). Utilizzando questi valori possiamo impostare l'ora corretta.

Codice di invio e ricezione del pacchetto

# 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)

Elaborazione dei dati dal server

L'elaborazione dei dati dal server è simile alle azioni del gentiluomo inglese dal vecchio problema di Raymond M. Smullyan (1978): “Un uomo non aveva un orologio da polso, ma a casa c'era un orologio da parete preciso, che a volte dimenticava avvolgere. Un giorno, avendo dimenticato di caricare di nuovo l'orologio, andò a trovare il suo amico, trascorse la serata con lui e quando tornò a casa riuscì a regolare correttamente l'orologio. Come è riuscito a farlo se il tempo di viaggio non era noto in anticipo? La risposta è: “Quando esce di casa, una persona carica l'orologio e ricorda in quale posizione si trovano le lancette. Venendo da un amico e lasciando gli ospiti, annota l'orario del suo arrivo e della sua partenza. Questo gli permette di scoprire per quanto tempo è stato in visita. Tornando a casa e guardando l'orologio, una persona determina la durata della sua assenza. Sottraendo da questo tempo il tempo trascorso in visita, una persona scopre il tempo impiegato nel viaggio di andata e ritorno. Aggiungendo la metà del tempo trascorso in viaggio al tempo di lasciare gli ospiti, ha l'opportunità di conoscere l'ora di arrivo a casa e regolare di conseguenza le lancette del suo orologio.

Trova l'ora in cui il server sta lavorando su una richiesta:

  1. Trova il tempo di viaggio del pacchetto dal client al server: ((Arrivo – Origine) – (Trasmissione – Ricevi)) / 2
  2. Trova la differenza tra l'ora del client e quella del server:
    Ricevi - Origina - ((Arrivo - Origine) - (Trasmetti - Ricevi)) / 2 =
    2 * Ricevi – 2 * Origina – Arrivo + Origine + Trasmetti – Ricevi =
    Ricevi – Origina – Arriva + Trasmetti

Aggiungiamo il valore risultante all'ora locale e ci godiamo la vita.

Uscita del risultato

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 collegamento.

Fonte: habr.com

Aggiungi un commento