Skriver en enkel NTP-klient

Hej, Hubbrukare. Idag vill jag berĂ€tta om hur man skriver en enkel NTP-klient. I huvudsak kommer vi att diskutera paketstrukturen och hur man behandlar svar frĂ„n NTP-servern. Koden kommer att skrivas i Python, för jag anser att det finns inget bĂ€ttre sprĂ„k för sĂ„dana uppgifter. KĂ€nnare kommer att mĂ€rka likheterna mellan koden och koden i ntplib — jag blev "inspirerad" av just den.

SÄ, vad Àr NTP egentligen? NTP Àr ett protokoll för kommunikation med servrar för exakt tid. Detta protokoll anvÀnds i mÄnga moderna maskiner. Till exempel, tjÀnsten w32tm i Windows.

Det finns totalt 5 versioner av NTP-protokollet. Den första, 0-versionen (1985, RFC958), anses för nÀrvarande vara förÄldrad. För nÀrvarande anvÀnds nyare versioner, 1 (1988, RFC1059), 2 (1989, RFC1119), 3 (1992, RFC1305) och 4 (1996, RFC2030). Versionerna 1-4 Àr kompatibla med varandra, de skiljer sig endast Ät i sina arbetsalgoritmer. servrar.

Paketformat

Skriver en enkel NTP-klient

Leap indicator (korrektionsindikator) — ett tal som visar en varning om koordineringssekunden. VĂ€rde:

  • 0 – ingen korrigering
  • 1 – den sista minuten pĂ„ dagen innehĂ„ller 61 sekunder
  • 2 – den sista minuten pĂ„ dagen innehĂ„ller 59 sekunder
  • 3 – serverfel (tiden Ă€r inte synkroniserad)

Version number (versionsnummer) – numret pĂ„ NTP-protokollet (1-4).

Mode (lĂ€ge) — lĂ€get för den som skickar paketet. VĂ€rden frĂ„n 0 till 7, de vanligaste:

  • 3 – klient
  • 4 – server
  • 5 – sĂ€ndningslĂ€ge

Stratum (lager) – antalet mellanliggande lager mellan servern och de referensur som anvĂ€nds (1 – servern hĂ€mtar data direkt frĂ„n referensuren, 2 – servern hĂ€mtar data frĂ„n en server med nivĂ„ 1 osv.).
Pool — ett heltal med tecken som representerar det maximala intervallet mellan pĂ„ varandra följande meddelanden. NTP-klienten anger hĂ€r intervallet med vilket den planerar att frĂ„ga servern, och NTP-servern – intervallet med vilket den planerar att bli tillfrĂ„gad. VĂ€rdet Ă€r lika med den binĂ€ra logaritmen av sekunder.
Precision (precision) — ett heltal med tecken som representerar noggrannheten hos systemklockorna. VĂ€rdet Ă€r lika med den binĂ€ra logaritmen av sekunder.
Root delay (serverfördröjning) – tiden det tar för klockans avlĂ€sningar att nĂ„ NTP-servern, angivet som ett antal sekunder med fast punkt.
Root dispersion (spridning av serveravlĂ€sningar) — spridningen av NTP-serverns klockavlĂ€sningar som ett antal sekunder med fast punkt.
Ref id (kĂ€llidentifierare) – klockans id. Om servern har stratum 1, sĂ„ Ă€r ref id – namnet pĂ„ atomklockan (4 ASCII-tecken). Om servern anvĂ€nder en annan server, sĂ„ anges adressen till den servern i ref id.
De senaste 4 fĂ€lten representerar tid – 32 bitar – heltal, 32 bitar – brĂ„kdel.
Referens – de senaste avlĂ€sningarna av klockan pĂ„ servern.
Ursprung – tiden nĂ€r paketet skickades (fylls i av servern – mer om detta nedan).
Mottagning – tiden dĂ„ servern tog emot paketet.
SĂ€nd – tiden dĂ„ paketet skickas frĂ„n servern till klienten (fylls i av klienten, mer om detta nedan).

De tvÄ sista fÀlten kommer vi inte att betrakta.

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):
        # NödvÀndigt för att ange skottsekund (2 bitar)
        self.leap_indicator = 0
        # Protokollversion (3 bitar)
        self.version_number = version_number
        # AvsÀndarens lÀge (3 bitar)
        self.mode = mode
        # NivÄn av "lager" lÀsningstid (1 byte)
        self.stratum = 0
        # Intervall mellan förfrÄgningar (1 byte)
        self.pool = 0
        # Precision (log2) (1 byte)
        self.precision = 0
        # Intervall för att klockan nÄr NTP-servern (4 bytes)
        self.root_delay = 0
        # Spridning av NTP-serverns klocka (4 bytes)
        self.root_dispersion = 0
        # Indikator för klockor (4 bytes)
        self.ref_id = 0
        # Senaste uppdateringstid pÄ servern (8 bytes)
        self.reference = 0
        # Tid för sÀndning av paket frÄn lokal maskin (8 bytes)
        self.originate = 0
        # Tid för mottagande pÄ server (8 bytes)
        self.receive = 0
        # Tid för sÀndning av svar frÄn server (8 bytes)
        self.transmit = transmit

För att skicka (och ta emot) paket till servern mÄste vi kunna omvandla det till en byte-array.
För denna (och omvĂ€nda) operation kommer vi att skriva tvĂ„ funktioner – pack() och unpack():

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

Funktionen unpack

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, som en app – kod som konverterar paketet till en fin strĂ€ng

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)

Skicka paketet till servern

Ett paket med ifyllda fĂ€lt mĂ„ste skickas till servern Version, Mode och SĂ€nd. I SĂ€nd det Ă€r nödvĂ€ndigt att ange aktuell tid pĂ„ den lokala maskinen (antal sekunder sedan 1 januari 1900), version – valfri mellan 1-4, lĂ€ge – 3 (klientlĂ€ge).

Servern, som tar emot begĂ€ran, fyller i alla fĂ€lt i NTP-paketet genom att kopiera vĂ€rdet till fĂ€ltet Ursprung vĂ€rdet frĂ„n SĂ€nd, som kommit i begĂ€ran. Jag förstĂ„r inte varför klienten inte kan fylla i sitt tidsvĂ€rde direkt i fĂ€ltet Ursprung. I slutĂ€ndan, nĂ€r paketet kommer tillbaka, har klienten 4 tidsvĂ€rden – tidpunkten för begĂ€ran (Ursprung), tiden dĂ„ begĂ€ran mottogs av servern (Mottagning), tiden dĂ„ svaret skickades av servern (SĂ€nd) och tiden dĂ„ svaret mottogs av klienten – AnlĂ€nder (ingĂ„r inte i paketet). Med hjĂ€lp av dessa vĂ€rden kan vi stĂ€lla in korrekt tid.

Kod för att skicka och ta emot paketet

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

Bearbetning av data frÄn servern

Databehandling frÄn servern liknar handlingarna av en engelsk gentleman i Raymond M. Smullyans gamla uppgift (1978): "En man hade ingen armbandsklocka, men hemma hÀngde det exakta vÀggklockor som han ibland glömde att dra upp. En dag, efter att ha glömt att dra upp klockan igen, besökte han en vÀn, tillbringade kvÀllen dÀr och nÀr han kom hem lyckades han stÀlla klockan rÀtt. Hur lyckades han göra det, om tiden för resan pÄ förhand var okÀnd?" Svaret Àr: "NÀr mannen gÄr ut ur huset drar han upp klockan och kommer ihÄg i vilken position visarna Àr. NÀr han kommer till sin vÀn och lÀmnar gÀstsalen noterar han tiden för sin ankomst och avfÀrd. Detta gör att han kan rÀkna ut hur lÀnge han varit pÄ besök. NÀr han ÄtervÀnder hem och ser pÄ klockan, avgör mannen lÀngden pÄ sin frÄnvaro. Genom att subtrahera den tid han tillbringade pÄ besök frÄn denna tid fÄr han reda pÄ tiden som spenderades pÄ resan till och frÄn. Genom att addera hÀlften av restiden till tiden för avfÀrd frÄn gÀsten, fÄr han möjlighet att ta reda pÄ tiden för ankomsten hem och justera sina klockans visare pÄ motsvarande sÀtt."

Vi finner serverns hanteringstid för begÀran:

  1. Vi finner paketets resa frĂ„n klienten till servern: ((Arrive – Originate) — (Transmit – Receive)) / 2
  2. Vi finner skillnaden mellan klientens och serverns tid:
    Receive — Originate — ((Arrive – Originate) — (Transmit – Receive)) / 2 =
    2 * Receive — 2 * Originate – Arrive + Originate + Transmit – Receive =
    Receive – Originate – Arrive + Transmit

Vi lÀgger till det erhÄllna vÀrdet till lokal tid och njuter av livet.

Resultatpresentation

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

Nyttig lÀnk.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster