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 finnes totalt 5 versjoner av NTP-protokollen. Den fþrste, versjon 0 (1985, RFC958), regnes for tiden som foreldet. Nyere versjoner er for tiden i bruk: versjon 1 (1988, RFC1059), versjon 2 (1989, RFC1119), versjon 3 (1992, RFC1305) og versjon 4 (1996, RFC2030). Versjon 1–4 er kompatible med hverandre; de ​​skiller seg bare i driftsalgoritmene. servere.

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

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