Att skriva en enkel NTP-klient

Hej habrausers. Idag vill jag prata om hur du skriver din egen enkla NTP-klient. I grund och botten kommer konversationen att vända sig till paketets struktur och hur svaret från NTP-servern behandlas. Koden kommer att skrivas i python, eftersom det enligt min mening helt enkelt inte finns något bättre språk för sådana saker. Konnässörer kommer att uppmärksamma likheten mellan koden och ntplib-koden - jag blev "inspirerad" av den.

Så vad är NTP egentligen? NTP är ett protokoll för att kommunicera med tidsservrar. Detta protokoll används i många moderna maskiner. Till exempel tjänsten w32tm i Windows.

Det finns 5 versioner av NTP-protokollet totalt. Den första, version 0 (1985, RFC958)), anses för närvarande vara föråldrad. Nyare används för närvarande, 1:a (1988, RFC1059), 2:a (1989, RFC1119), 3:a (1992, RFC1305) och 4:a (1996, RFC2030). Versioner 1-4 är kompatibla med varandra, de skiljer sig endast i servrarnas algoritmer.

Paketformat

Att skriva en enkel NTP-klient

Språngindikator (korrigeringsindikator) är en siffra som indikerar skottsekundsvarningen. Menande:

  • 0 – ingen korrigering
  • 1 - dagens sista minut innehåller 61 sekunder
  • 2 - dagens sista minut innehåller 59 sekunder
  • 3 - serverfel (tid ur synkronisering)

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

Mode (läge) — driftläge för paketsändaren. Värde från 0 till 7, vanligast:

  • 3 - klient
  • 4 - server
  • 5 – sändningsläge

Skikt (lagernivå) - antalet mellanliggande lager mellan servern och referensklockan (1 - servern tar data direkt från referensklockan, 2 - servern tar data från servern med nivå 1, etc.).
Pool är ett signerat heltal som representerar det maximala intervallet mellan på varandra följande meddelanden. NTP-klienten anger här intervallet med vilket den förväntar sig att polla servern, och NTP-servern anger intervallet med vilket den förväntar sig att bli pollad. Värdet är lika med den binära logaritmen av sekunder.
Precision (noggrannhet) är ett heltal med tecken som representerar noggrannheten hos systemklockan. Värdet är lika med den binära logaritmen av sekunder.
rotfördröjning (serverlatens) är den tid det tar för klockan att nå NTP-servern, som ett fast antal sekunder.
rotspridning (serverspridning) - Spridningen av NTP-serverklockan som ett antal sekunder med fast punkt.
Ref id (källidentifierare) – klock-id. Om servern har stratum 1 är ref id namnet på atomklockan (4 ASCII-tecken). Om servern använder en annan server, innehåller ref-id:t adressen till denna server.
De sista 4 fälten representerar tiden - 32 bitar - heltalsdelen, 32 bitar - bråkdelen.
Hänvisning - den senaste klockan på servern.
Härröra – tidpunkt då paketet skickades (ifyllt av servern – mer om det nedan).
Motta – tidpunkt då paketet togs emot av servern.
Sänd – tidpunkt då paketet skickades från servern till klienten (fylls i av klienten, mer om det nedan).

De två sista fälten kommer inte att beaktas.

Låt oss skriva vårt paket:

Paketkod

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

För att skicka (och ta emot) ett paket till servern måste vi kunna omvandla det till en array av byte.
För denna (och omvända) operation kommer vi att skriva två funktioner - pack() och unpack():

packfunktion

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

uppackningsfunktion

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

För lata människor, som en applikation - kod som förvandlar paketet till ett vackert snöre

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)

Skickar ett paket till servern

Skicka ett paket med ifyllda fält till servern version, Mode и Sänd. I Sänd du måste ange aktuell tid på den lokala maskinen (antal sekunder sedan 1 januari 1900), version - någon av 1-4, läge - 3 (klientläge).

Servern, efter att ha tagit emot begäran, fyller i alla fält i NTP-paketet och kopierar in i fältet Härröra värde från Sänd, som kom i begäran. Det är för mig ett mysterium varför klienten inte omedelbart kan fylla i värdet av sin tid i fält Härröra. Som ett resultat, när paketet kommer tillbaka, har klienten 4 tidsvärden - tiden då begäran skickades (Härröra), tiden då servern tog emot begäran (Motta), tiden då servern skickade svaret (Sänd) och när kunden fick svaret – Anlända (inte i paketet). Med dessa värden kan vi ställa in rätt tid.

Kod för att skicka och ta emot paket

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

Databehandling från servern

Behandlingen av data från servern liknar den engelska gentlemannens agerande från Raymond M. Smallians gamla problem (1978): "En person hade inte ett armbandsur, men det fanns en korrekt väggklocka hemma, som han glömde ibland att vinda. En dag, när han glömde att starta klockan igen, gick han och hälsade på sin vän, tillbringade kvällen med honom, och när han kom hem lyckades han ställa in klockan korrekt. Hur lyckades han göra detta om restiden inte var känd i förväg? Svaret är: "När en person lämnar huset rullar en person upp klockan och kommer ihåg visarnas position. Efter att ha kommit till en vän och lämnat gästerna, noterar han tidpunkten för sin ankomst och avgång. Detta gör att han kan ta reda på hur länge han var på besök. När han återvänder hem och tittar på klockan, bestämmer en person varaktigheten av sin frånvaro. Om man från denna tid drar ifrån tiden som han tillbringade med att besöka, får personen reda på tiden som spenderats på vägen fram och tillbaka. Genom att lägga till halva tiden på vägen till tiden för att lämna gästerna får han möjlighet att ta reda på ankomsttiden hem och justera visarna på sin klocka därefter.

Hitta den tid då servern arbetade med begäran:

  1. Hitta paketrestiden från klienten till servern: ((Ankomst - Ursprung) - (Sänd - Ta emot)) / 2
  2. Hitta skillnaden mellan klient- och servertid:
    Ta emot - Ursprung - ((Ankomst - Ursprung) - (Sänd - Ta emot)) / 2 =
    2 * Ta emot - 2 * Ursprung - Anlända + Ursprunga + Sänd - Ta emot =
    Ta emot - Ursprung - Ankomst + Sänd

Vi tillför det mottagna värdet till den lokala tiden och njuter av livet.

Resultat Utdata

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)

Användbar länk.

Källa: will.com

Lägg en kommentar