At skrive en simpel NTP-klient

Hej, Habrausers. I dag vil jeg tale om, hvordan du skriver din egen simple NTP-klient. Grundlæggende vil samtalen dreje sig om pakkens struktur og metoden til behandling af svaret fra NTP-serveren. Koden vil blive skrevet i Python, fordi det forekommer mig, at der simpelthen ikke findes noget bedre sprog til sådanne ting. Kendere vil bemærke ligheden mellem koden og ntplib-koden - jeg blev "inspireret" af den.

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

Der er 5 versioner af NTP-protokollen i alt. Den første, version 0 (1985, RFC958)), anses i øjeblikket for at være forældet. Nu bruges de nyere, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) og 4. (1996, RFC2030). Versioner 1-4 er kompatible med hinanden; de adskiller sig kun i serverdriftsalgoritmerne.

Pakkeformat

At skrive en simpel NTP-klient

Spring indikator (korrektionsindikator) - et tal, der angiver en advarsel om koordinationssekundet. Betyder:

  • 0 – ingen korrektion
  • 1 – dagens sidste minut indeholder 61 sekunder
  • 2 – dagens sidste minut indeholder 59 sekunder
  • 3 – serverfejl (tiden er ikke synkroniseret)

Versionsnummer (versionsnummer) – NTP-protokolversionsnummer (1-4).

tilstand (tilstand) — driftstilstand for pakkeafsenderen. Værdi fra 0 til 7, mest almindelig:

  • 3 – klient
  • 4 – server
  • 5 – udsendelsestilstand

stratum (lagniveau) – antallet af mellemliggende lag mellem serveren og referenceuret (1 – serveren tager data direkte fra referenceuret, 2 – serveren tager data fra en server med lag 1 osv.).
pool er et signeret heltal, der repræsenterer det maksimale interval mellem på hinanden følgende meddelelser. NTP-klienten angiver her det interval, hvormed den forventer at polle serveren, og NTP-serveren angiver det interval, hvormed den forventer at blive pollet. Værdien er lig med den binære logaritme af sekunder.
Precision (nøjagtighed) er et fortegnet heltal, der repræsenterer nøjagtigheden af ​​systemuret. Værdien er lig med den binære logaritme af sekunder.
Root forsinkelse (serverforsinkelse) – den tid, det tager for uraflæsningerne at nå NTP-serveren, som et fast antal sekunder.
Rodspredning (serverspredning) - spredning af NTP-serverens uraflæsninger som et antal sekunder med et fast punkt.
Ref id (kildeidentifikator) – ur-id. Hvis serveren har stratum 1, så er ref id navnet på atomuret (4 ASCII-tegn). Hvis serveren bruger en anden server, så indeholder ref id adressen på denne server.
De sidste 4 felter repræsenterer tiden - 32 bit - heltalsdelen, 32 bits - brøkdelen.
Henvisning — de seneste uraflæsninger på serveren.
Oprindelse – tidspunkt for afsendelse af pakken (udfyldt af serveren - mere om dette nedenfor).
Modtage – tidspunktet, hvor pakken blev modtaget af serveren.
Transmit – tidspunkt for afsendelse af pakken fra serveren til klienten (udfyldt af klienten, mere om dette nedenfor).

Vi vil ikke overveje de to sidste felter.

Lad os skrive vores pakke:

Pakkekode

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

For at sende (og modtage) en pakke til serveren skal vi være i stand til at omdanne den til et byte-array.
Til denne (og omvendt) operation vil vi skrive to funktioner - pack() og unpack():

pakkefunktion

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

udpakningsfunktion

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

For dovne mennesker, som en applikation - kode, der gør en pakke til en smuk snor

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)

Sender en pakke til serveren

En pakke med udfyldte felter skal sendes til serveren Udgave, tilstand и Transmit. I Transmit du skal angive den aktuelle tid på den lokale maskine (antallet af sekunder siden 1. januar 1900), version - enhver af 1-4, tilstand - 3 (klienttilstand).

Serveren, efter at have accepteret anmodningen, udfylder alle felterne i NTP-pakken og kopierer ind i feltet Oprindelse værdi fra Transmit, som kom i anmodningen. Det er mig en gåde, hvorfor klienten ikke umiddelbart kan udfylde værdien af ​​sin tid i feltet Oprindelse. Som et resultat, når pakken kommer tilbage, har klienten 4 tidsværdier - det tidspunkt, hvor anmodningen blev sendt (Oprindelse), tidspunktet serveren modtog anmodningen (Modtage), tidspunktet serveren sendte svaret (Transmit) og det tidspunkt, hvor klienten modtog svaret – Ankomme (ikke i pakken). Ved at bruge disse værdier kan vi indstille det korrekte tidspunkt.

Kode til afsendelse og modtagelse af pakke

# 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 ligner den engelske gentlemans handlinger fra Raymond M. Smullyans gamle problem (1978): “En mand havde ikke et armbåndsur, men der var et nøjagtigt vægur derhjemme, som han nogle gange glemte. at vinde. En dag, da han havde glemt at trække sit ur igen, gik han for at besøge sin ven, tilbragte aftenen med ham, og da han kom hjem, lykkedes det ham at stille uret korrekt. Hvordan klarede han det, hvis rejsetiden ikke var kendt på forhånd? Svaret er: ”Når man forlader hjemmet, trækker en person sit ur og husker, hvilken position viserne er i. Efter at være kommet til en ven og forladt gæsterne, noterer han tidspunktet for sin ankomst og afgang. Dette giver ham mulighed for at finde ud af, hvor længe han var på besøg. Når han vender hjem og ser på uret, bestemmer en person varigheden af ​​sit fravær. Ved at trække fra denne tid den tid, han brugte på at besøge, finder en person ud af den tid, han brugte på at rejse frem og tilbage. Ved at lægge halvdelen af ​​tiden på vejen til det tidspunkt, hvor han forlader gæsterne, får han mulighed for at finde ud af, hvornår han kommer hjem og justere viserne på sit ur derefter.”

Find det tidspunkt, hvor serveren arbejder på en anmodning:

  1. Find rejsetiden for pakken fra klienten til serveren: ((Ankomst – Oprind) – (Send – Modtag)) / 2
  2. Find forskellen mellem klient- og servertid:
    Modtag - Oprind - ((Ankommer - Oprind) - (Send - Modtag)) / 2 =
    2 * Modtag – 2 * Oprinde – Ankomst + Oprinde + Send – Modtag =
    Modtag – Oprinde – Ankomst + Send

Vi tilføjer den resulterende værdi til den lokale tid og nyder livet.

Output af resultatet

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)

Nyttig link.

Kilde: www.habr.com

Tilføj en kommentar