Skrive en enkel NTP-klient

Hei habrabrukere. I dag vil jeg snakke om hvordan du skriver din egen enkle NTP-klient. I utgangspunktet vil samtalen dreie seg om strukturen til pakken og hvordan svaret fra NTP-serveren behandles. Koden vil bli skrevet i python, fordi det etter min mening rett og slett ikke finnes noe bedre språk for slike ting. Kjennere vil ta hensyn til likheten mellom koden og ntplib-koden - jeg ble "inspirert" av den.

Så hva er NTP egentlig? NTP er en protokoll for kommunikasjon med tidsservere. Denne protokollen brukes i mange moderne maskiner. For eksempel w32tm-tjenesten på Windows.

Det er 5 versjoner av NTP-protokollen totalt. Den første, versjon 0 (1985, RFC958) anses for øyeblikket som foreldet. Nyere brukes for tiden, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) og 4. (1996, RFC2030). Versjoner 1-4 er kompatible med hverandre, de skiller seg bare i algoritmene til serverne.

Pakkeformat

Skrive en enkel NTP-klient

Sprangindikator (korreksjonsindikator) er et tall som indikerer sprangsekundvarselet. Betydning:

  • 0 - ingen korreksjon
  • 1 - dagens siste minutt inneholder 61 sekunder
  • 2 - dagens siste minutt inneholder 59 sekunder
  • 3 - serverfeil (tid ute av synkronisering)

Versjonsnummer (versjonsnummer) – NTP-protokollens versjonsnummer (1-4).

Mote (modus) — driftsmodus for pakkesenderen. Verdi fra 0 til 7, vanligst:

  • 3 - klient
  • 4 - server
  • 5 - kringkastingsmodus

stratum (lagnivå) - antall mellomlag mellom serveren og referanseklokken (1 - serveren tar data direkte fra referanseklokken, 2 - serveren tar data fra serveren med nivå 1 osv.).
Basseng er et signert heltall som representerer det maksimale intervallet mellom påfølgende meldinger. NTP-klienten spesifiserer her intervallet den forventer å polle serveren med, og NTP-serveren intervallet den forventer å bli pollet med. Verdien er lik den binære logaritmen av sekunder.
Precision (presisjon) er et fortegnet heltall som representerer nøyaktigheten til systemklokken. Verdien er lik den binære logaritmen av sekunder.
rotforsinkelse (serverlatens) er tiden det tar for klokken å nå NTP-serveren, som et fast punkt antall sekunder.
rotspredning (serverspredning) - Spredningen av NTP-serverklokken som et fast punkt antall sekunder.
Ref id (kilde-id) – se-id. Hvis serveren har stratum 1, er ref id navnet på atomuret (4 ASCII-tegn). Hvis serveren bruker en annen server, inneholder ref-ID-en adressen til denne serveren.
De siste 4 feltene representerer tiden - 32 biter - heltallsdelen, 32 biter - brøkdelen.
Referanse - den siste klokken på serveren.
Opprinnelse – tidspunktet da pakken ble sendt (utfylt av serveren – mer om det nedenfor).
Motta – tidspunktet da pakken ble mottatt av serveren.
Overføre – tidspunktet da pakken ble sendt fra serveren til klienten (utfylt av klienten, mer om det nedenfor).

De to siste feltene vil ikke bli vurdert.

La oss skrive pakken vår:

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 å sende (og motta) en pakke til serveren, må vi kunne gjøre den om til en rekke byte.
For denne (og omvendt) operasjonen vil vi skrive to funksjoner - pack() og unpack():

pakkefunksjon

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

pakke ut funksjon

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 late mennesker, som en applikasjon - kode som gjør pakken til en vakker streng

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

Send en pakke med utfylte felt til serveren Versjon, Mote и Overføre. I Overføre du må spesifisere gjeldende tid på den lokale maskinen (antall sekunder siden 1. januar 1900), versjon - hvilken som helst av 1-4, modus - 3 (klientmodus).

Serveren, etter å ha mottatt forespørselen, fyller ut alle feltene i NTP-pakken og kopierer inn i feltet Opprinnelse verdi fra Overføre, som kom i forespørselen. Det er et mysterium for meg hvorfor klienten ikke umiddelbart kan fylle ut verdien av sin tid i felten Opprinnelse. Som et resultat, når pakken kommer tilbake, har klienten 4 tidsverdier - tidspunktet forespørselen ble sendt (Opprinnelse), tidspunktet serveren mottok forespørselen (Motta), tidspunktet serveren sendte svaret (Overføre) og tidspunktet for mottak av et svar fra klienten - Ankomme (ikke i pakken). Med disse verdiene kan vi stille inn riktig tid.

Kode for sending og mottak av 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)

Databehandling fra serveren

Behandlingen av data fra serveren ligner handlingene til den engelske gentlemannen fra det gamle problemet til Raymond M. Smallian (1978): «En person hadde ikke et armbåndsur, men det var en nøyaktig veggklokke hjemme, som han noen ganger glemte å vind. En dag, da han glemte å starte klokken igjen, dro han for å besøke vennen sin, tilbrakte kvelden med ham, og da han kom hjem klarte han å stille klokken riktig. Hvordan klarte han å gjøre dette hvis reisetiden ikke var kjent på forhånd? Svaret er: «Når en person forlater huset, trekker en opp klokken og husker posisjonen til viserne. Når han kommer til en venn og forlater gjestene, noterer han tidspunktet for ankomst og avreise. Dette lar ham finne ut hvor lenge han var borte. Når han kommer hjem og ser på klokken, bestemmer en person varigheten av fraværet. Ved å trekke fra denne tiden tiden han brukte på besøk, finner personen ut tiden han brukte på veien frem og tilbake. Ved å legge til halvparten av tiden brukt på veien til tidspunktet for å forlate gjestene, får han muligheten til å finne ut når han kommer hjem og justere viserne på klokken deretter.

Finn tidspunktet serveren jobbet med forespørselen:

  1. Finne pakkereisetiden fra klienten til serveren: ((Ankomme - Opprinnelse) - (Sende - Motta)) / 2
  2. Finn forskjellen mellom klient- og servertid:
    Motta - Opprinne - ((Ankomme - Opprinne) - (Sende - Motta)) / 2 =
    2 * Motta - 2 * Opprinne - Ankomme + Opprinne + Sende - Motta =
    Motta - Opprinne - Ankomme + Send

Vi legger den mottatte verdien til den lokale tiden og nyter livet.

Resultatutgang

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

Legg til en kommentar