Skriver en simpel NTP-klient

Hej, Habra-brugere. I dag vil jeg gerne fortælle om, hvordan man skriver sin egen enkle NTP-klient. Vi vil primært tale om pakkestrukturen og metoden til at behandle svar fra NTP-serveren. Koden vil blive skrevet i Python, fordi jeg mener, at der simpelthen ikke findes et bedre sprog til sådanne ting. Kenderne vil bemærke ligheden mellem koden og koden fra ntplib - jeg har faktisk 'låt mig inspirere' af det.

Så hvad er NTP egentlig? NTP er en protokol til interaktion med præcise tidsservere. Denne protokol bruges i mange moderne maskiner. For eksempel tjenesten w32tm i Windows.

Der findes i alt 5 versioner af NTP-protokollen. Den første, 0. version (1985, RFC958), betragtes i øjeblikket som forældet. I dag anvendes nyere versioner, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) og 4. (1996, RFC2030). Versionerne 1-4 er kompatible med hinanden; de adskiller sig kun i arbejdets algoritmer. servere.

Pakkens format

Skriver en simpel NTP-klient

Leap indicator (korrektionsindikator) - et tal, der viser en advarsel om sekundet for koordineret tid. Værdi:

  • 0 - ingen korrektion
  • 1 - den sidste minut i dagen indeholder 61 sekunder
  • 2 - den sidste minut i dagen indeholder 59 sekunder
  • 3 - serverfejl (tiden er ikke synkroniseret)

Versionsnummer (versionsnummer) - protokollens version af NTP (1-4).

Mode (driftsmode) - afsenderens pakkeopereringsmode. Værdi fra 0 til 7, de mest almindelige:

  • 3 - klient
  • 4 - server
  • 5 - multicast mode

Stratum (lag) - antallet af mellemled mellem serveren og referenceklokkerne (1 - serveren henter data direkte fra referenceklokker, 2 - serveren henter data fra en 1-lags server osv.).
Pool et helt tal med tegn, der repræsenterer det maksimale interval mellem på hinanden følgende meddelelser. NTP-klienten angiver her intervallet, hvormed den planlægger at forespørge serveren, mens NTP-serveren angiver intervallet, hvormed den forventer at blive forespurgt. Værdien er lig med den binære logaritme af sekunder.
Precision (præcision) - et helt tal med tegn, der repræsenterer præcisionen af systemklokkerne. Værdien er lig med den binære logaritme af sekunder.
Root delay (serverforsinkelse) - den tid, det tager for klokkeslæt at nå frem til NTP-serveren, som et antal sekunder med fast komma.
Root dispersion (serverens spredning) - spredningen af NTP-serverens klokkeslæt som et antal sekunder med fast komma.
Ref id (kildeidentifier) – id for klokken. Hvis serveren har stratum 1, så er ref id – navnet på atomklokken (4 ASCII-tegn). Hvis serveren bruger en anden server, så er adressen på den server angivet i ref id.
De sidste 4 felter repræsenterer tid – 32 bit – heltalsdel, 32 bit – brøkdelsdel.
Reference – de sidste målinger fra klokken på serveren.
Originate – tidspunktet hvor pakken blev sendt (udfyldes af serveren – mere om det nedenfor).
Receive – tidspunktet for modtagelse af pakken af serveren.
Transmit – tidspunktet for afsendelse af pakken fra serveren til klienten (udfyldes af klienten, også mere om det nedenfor).

De to sidste felter vil vi ikke overveje.

Lad os skrive vores pakke:

Pakkekode

class NTPPacket:
    _FORMAT = "!B B b b 11I"

    def __init__(self, version_number=2, mode=3, transmit=0):
        # Nødvendigt at klarlægge leap second (2 bits)
        self.leap_indicator = 0
        # Protokolversion (3 bits)
        self.version_number = version_number
        # Afsenderens tilstand (3 bits)
        self.mode = mode
        # Niveauet for "lagdeling" læsningstid (1 byte)
        self.stratum = 0
        # Interval mellem anmodninger (1 byte)
        self.pool = 0
        # Præcision (log2) (1 byte)
        self.precision = 0
        # Interval for klokken, nå NTP-serveren (4 bytes)
        self.root_delay = 0
        # Spridning af klokken NTP-server (4 bytes)
        self.root_dispersion = 0
        # Indikator for klokker (4 bytes)
        self.ref_id = 0
        # Sidste opdateringstid på serveren (8 bytes)
        self.reference = 0
        # Tidspunkt for pakken sendt fra den lokale maskine (8 bytes)
        self.originate = 0
        # Tidspunkt for modtagelse på serveren (8 bytes)
        self.receive = 0
        # Tidspunkt for afsendelse af svar fra serveren (8 bytes)
        self.transmit = transmit

For at sende (og modtage) en pakke til serveren, skal vi kunne konvertere den til en byte-array.
Til denne (og omvendte) operation skriver vi to funktioner – pack() og unpack():

Pack-funktion

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

Unpack-funktion

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

Til dovne, som en bonus – koden, der omdanner pakken til en pæn tekststreng

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)

Send pakke til serveren

Der skal sendes en pakke til serveren med udfyldte felter Version, Mode og Transmit. I Transmit det er nødvendigt at angive det aktuelle tidspunkt på den lokale maskine (antal sekunder siden 1. januar 1900), version – en af 1-4, mode – 3 (klientmode).

Serveren, der modtager anmodningen, udfylder alle felter i NTP-pakken ved at kopiere værdien til feltet Originate fra værdien i Transmit, som kom med anmodningen. Det er et mysterium for mig, hvorfor klienten ikke kan udfylde sin tid direkte i feltet Originate. Når pakken kommer tilbage, har klienten 4 tidsværdier – tidspunktet for anmodningens afsendelse (Originate), tidspunktet for modtagelse af anmodningen af serveren (Receive), tidspunktet for serverens afsendelse af svaret (Transmit) og tidspunktet for modtagelse af svaret af klienten – Ankom (ikke i pakken). Ved hjælp af disse værdier kan vi fastsætte det korrekte tidspunkt.

Koden til sending og modtagelse af pakken

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

Behandling af data fra serveren

Behandling af data fra serveren minder om handlingerne fra en engelsk gentleman i Raymond M. Smalleys gamle opgave (1978): «En person havde ikke noget armbåndsur, men derhjemme hang der præcise vægure, som han nogle gange glemte at wound. En dag, efter at have glemt at winding uret igen, gik han på besøg hos en ven, tilbragte aftenen der, og da han kom hjem, kunne han korrekt indstille uret. Hvordan lykkedes det ham at gøre dette, hvis rejsetiden på forhånd ikke var kendt?» Svaret er: «Da han forlod huset, windede manden uret og huskede, i hvilken position viserne var. Da han kom til sin ven og forlod gæsten, noterede han tidspunkterne for sin ankomst og afgang. Dette giver ham mulighed for at vide, hvor længe han havde været på besøg. Da han kom hjem og så på uret, bestemte manden varigheden af sin fravær. Ved at trække den tid fra, han havde brugt hos gæsten, lærer han tidsforbruget på turen frem og tilbage. Ved at lægge halvdelen af turens tidsforbrug til tiden for at forlade gæsten, kan han finde ud af, hvornår han kom hjem, og justere uret derefter.»

Finder serverens behandlingstid for anmodningen:

  1. Finder pakkens rejsetid fra klienten til serveren: ((Ankomst - Oprindelse) - (Transmit - Modtag)) / 2
  2. Finder forskellen mellem klientens og serverens tid:
    Modtag - Oprindelse - ((Ankomst - Oprindelse) - (Transmit - Modtag)) / 2 =
    2 * Modtag - 2 * Oprindelse - Ankomst + Oprindelse + Transmit - Modtag =
    Modtag - Oprindelse - Ankomst + Transmit

Tilføjer den opnåede værdi til den lokale tid og nyder livet.

Udskrivning af resultatet

time_different = answer.get_time_different(arrive_time)
result = "Tidsforskel: {}nServer tid: {}n{}".format(
    time_different,
    datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
    answer.to_display())
print(result)

Nyttig link.

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster