Vi skriver en enkel NTP-klient

Hei, Habr-brukere. I dag vil jeg fortelle om hvordan man kan lage sin egen enkle NTP-klient. Hovedfokuset vil vĂŠre pĂ„ pakkestrukturen og metoden for Ă„ hĂ„ndtere svaret fra NTP-serveren. Koden vil bli skrevet i Python, fordi jeg mener det ikke finnes noe bedre sprĂ„k for slike oppgaver. Kjennerne vil legge merke til likheten mellom koden og koden i ntplib — jeg ble inspirert av nettopp det.

SĂ„, hva er egentlig NTP? NTP er en protokoll for interaksjon med presise tidservere. Denne protokollen brukes i mange moderne maskiner. For eksempel, w32tm-tjenesten i Windows.

Det finnes totalt 5 versjoner av NTP-protokollen. Den fĂžrste, versjon 0 (1985, RFC958), anses for tiden som foreldet. NĂ„ brukes nyere versjoner, 1 (1988, RFC1059), 2 (1989, RFC1119), 3 (1992, RFC1305) og 4 (1996, RFC2030). Versjonene 1-4 er kompatible med hverandre og skiller seg kun i arbeidsalgoritmer. servere.

Pakkefunksjon

Vi skriver en enkel NTP-klient

Leap indicator (korreksjonsindikator) — et tall som viser en advarsel om koordinert tid. Verdi:

  • 0 – ingen korreksjon
  • 1 – den siste minuttet av dagen inneholder 61 sekunder
  • 2 – den siste minuttet av dagen inneholder 59 sekunder
  • 3 – serverfeil (tiden er ikke synkronisert)

Version number (versjonsnummer) – versjonsnummeret for NTP-protokollen (1-4).

Mode (modus) — senderens driftsmodus. Verdi fra 0 til 7, de mest vanlige:

  • 3 – klient
  • 4 – server
  • 5 – kringkastingsmodus

Stratum (lagdyp) – antall mellomliggende lag mellom serveren og referansetidene (1 – serveren henter data direkte fra referansetidene, 2 – serveren henter data fra en server pĂ„ lag 1, osv.).
Pool — et heltall med fortegn som representerer det maksimale intervallet mellom pĂ„fĂžlgende meldinger. NTP-klienten angir intervallet som den planlegger Ă„ forespĂžrre serveren, mens NTP-serveren angir intervallet for nĂ„r den forventer Ă„ bli forespurt. Verdien er lik den binĂŠre logaritmen av sekunder.
Precision (presisjon) — et heltall med fortegn som representerer presisjonen til systemklokkene. Verdien er lik den binére logaritmen av sekunder.
Root delay (serverforsinkelse) – tiden det tar for klokkeslettene Ă„ nĂ„ NTP-serveren, i antall sekunder med fast desimal.
Root dispersion (spridningen av serverlesninger) — spridningen av tidene fra NTP-serveren, i antall sekunder med fast desimal.
Ref id (kildeidentifikator) – id timer. Hvis serveren har stratum 1, er ref id navnet pĂ„ atomklokken (4 ASCII-tegn). Hvis serveren bruker en annen server, er adressen til denne serveren registrert i ref id.
De siste 4 feltene representerer tid – 32 biter – heltall, 32 biter – brþkdeler.
Referanse – de siste avlesningene fra klokken pĂ„ serveren.
Opprinnelse – tiden pakken ble sendt (fylles ut av serveren – mer om dette nedenfor).
Mottak – tiden pakken ble mottatt av serveren.
Send – tiden pakken ble sendt fra serveren til klienten (fylles ut av klienten, ogsĂ„ mer om dette nedenfor).

De to siste feltene vil vi ikke vurdere.

La oss skrive pakken vÄr:

Pakke kode

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

    def __init__(self, version_number=2, mode=3, transmit=0):
        # NĂždvendig for Ă„ angi skuddsekund (2 biter)
        self.leap_indicator = 0
        # Protokollversjon (3 biter)
        self.version_number = version_number
        # Senderens modus (3 biter)
        self.mode = mode
        # NivÄet for "lag" avlesningstid (1 byte)
        self.stratum = 0
        # Intervall mellom forespĂžrselen (1 byte)
        self.pool = 0
        # Presisjon (log2) (1 byte)
        self.precision = 0
        # Intervall for klokken som nÄr NTP-serveren (4 bytes)
        self.root_delay = 0
        # Spredning av klokken til NTP-serveren (4 bytes)
        self.root_dispersion = 0
        # Indikator for klokker (4 bytes)
        self.ref_id = 0
        # Siste oppdateringstid pÄ serveren (8 bytes)
        self.reference = 0
        # Tid for sending av pakken fra lokal maskin (8 bytes)
        self.originate = 0
        # Tid for mottak pÄ server (8 bytes)
        self.receive = 0
        # Tid for sending av svaret fra server (8 bytes)
        self.transmit = transmit

For Ä sende (og motta) pakken til serveren, mÄ vi kunne konvertere den til et bytearray.
For denne (og den omvendte) operasjonen vil vi skrive to funksjoner – pack() og unpack():

Pack-funksjonen

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

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 the lazy, as an application – code that converts the packet into a neat string.

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)

Sending a packet to the server

A packet with filled fields needs to be sent to the server. Version, Mode og Send. I Send You need to specify the current time on the local machine (number of seconds since January 1, 1900), version - any from 1-4, mode - 3 (client mode).

The server, upon receiving the request, fills in all fields in the NTP packet by copying the value into the field Opprinnelse from the Send, received in the request. It is a mystery to me why the client cannot immediately fill in its time value in the field Opprinnelse. As a result, when the packet returns, the client has 4 time values – the time of the request sent (Opprinnelse), the time of the request received by the server (Mottak), the time the response is sent by the server (Send) and the time the response is received by the client – Arrive (not in the packet). With these values, we can set the correct time.

Code for sending and receiving the packet

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

Processing data from the server

Behandling av data fra serveren er sammenlignbar med handlingene til en engelsk gentleman fra Raymond M. Smullyans gamle oppgave (1978): «En mann hadde ikke armbÄndsur, men hjemme hang det nÞyaktige klokker som han noen ganger glemte Ä stille. En gang, etter Ä ha glemt Ä stille klokken igjen, dro han pÄ besÞk til en venn, tilbrakte kvelden der, og da han kom hjem, klarte han Ä stille klokken riktig. Hvordan klarte han det, nÄr reisen dit ikke var kjent pÄ forhÄnd?» Svaret er: «NÄr mannen forlater huset, stiller han klokken og husker posisjonen til viserne. NÄr han ankommer vennens hus og drar derfra, merker han tiden for ankomst og avreise. Dette gjÞr at han kan vite hvor lenge han var pÄ besÞk. NÄr han kommer hjem og ser pÄ klokken, avgjÞr han varigheten av sitt fravÊr. Ved Ä trekke fra den tiden han tilbrakte pÄ besÞk, fÄr han tiden brukt pÄ reisen frem og tilbake. Ved Ä legge til halvparten av reisetiden til tiden for avreise fra besÞket, kan han finne ut nÄr han kom hjem og stille klokken deretter.»

Finn tiden serveren bruker pÄ forespÞrselen:

  1. Finn tiden pakken bruker fra klienten til serveren: ((Ankomme – Opprette) — (Overfþre – Motta)) / 2
  2. Finn forskjellen mellom klientens tid og serverens:
    Motta — Opprette — ((Ankomme – Opprette) — (Overfþre – Motta)) / 2 =
    2 * Motta — 2 * Opprette – Ankomme + Opprette + Overfþre – Motta =
    Motta – Opprette – Ankomme + Overfþre

Legg til det oppnÄdde verdien til lokal tid og gled deg over livet.

Vis resultatet

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

Nyttig lenke.

Kilde: habr.com

KjĂžp pĂ„litelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere đŸ”„ KjĂžp pĂ„litelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster