Shkruajmë një klient të thjeshtë NTP

Përshëndetje, përdorues të Habraya. Sot dua të flas për atë se si të shkruajmë klientin tonë të thjeshtë NTP. Kryesisht, biseda do të shkojë për strukturën e paketës dhe metodën e përpunimit të përgjigjes nga serveri NTP. Kodi do të shkruhet në Python, sepse mendoj se nuk ka gjuhë më të mirë për gjëra të tilla. Njohësit do të vërejnë ngjashmërinë e kodit me kodin e ntplib - për këtë isha "inspiruar" pikërisht prej tij.

Pra, çfarë është në të vërtetë NTP? NTP është një protokoll për ndërveprimin me serverët e saktësisë së kohës. Ky protokoll përdoret në shumë makina moderne. Për shembull, shërbimi w32tm në Windows.

Në total ekzistojnë 5 versione të protokollit NTP. Versioni i parë, versioni 0 (1985, RFC958), tani konsiderohet i vjetruar. Tani përdoren versione më të reja, 1 (1988, RFC1059), 2 (1989, RFC1119), 3 (1992, RFC1305) dhe 4 (1996, RFC2030). Versionet 1-4 janë të pajtueshme me njëra-tjetrën, ato ndryshojnë vetëm në algoritmet e funksionimit. serverësh.

Formati i paketës

Shkruajmë një klient të thjeshtë NTP

Indikatori i leap (indikatori i korrigjimit) — njĂ« numĂ«r qĂ« tregon njĂ« paralajmĂ«rim pĂ«r sekondĂ«n e koordinatĂ«s. Vlera:

  • 0 – nuk ka korrigjim
  • 1 – minuta e fundit e ditĂ«s pĂ«rmban 61 sekonda
  • 2 – minuta e fundit e ditĂ«s pĂ«rmban 59 sekonda
  • 3 – defekt nĂ« server (koha nuk Ă«shtĂ« e sinkronizuar)

Numri i versionit (numri i versionit) – numri i versionit tĂ« protokollit NTP (1-4).

Rezhimi (rezhimi) — mĂ«nyra e funksionimit tĂ« dĂ«rguesit tĂ« paketĂ«s. Vlera nga 0 nĂ« 7, mĂ« tĂ« zakonshmet:

  • 3 – klient
  • 4 – server
  • 5 – rezhimi widecast

Stratum (niveli i stratum) – numri i shtresave ndĂ«rmjet serverit dhe orave referuese (1 – serveri merr tĂ« dhĂ«nat direkt nga orĂ«t referuese, 2 – serveri merr tĂ« dhĂ«nat nga njĂ« server me nivel 1, etj.).
Pool — njĂ« numĂ«r tĂ«rĂ« me shenjĂ«, qĂ« pĂ«rfaqĂ«son intervalin maksimal midis mesazheve tĂ« njĂ«pasnjĂ«shme. Klienti NTP e tregon kĂ«tu intervalin me tĂ« cilin ai pret tĂ« pyese serverin, ndĂ«rsa serveri NTP e tregon intervalin me tĂ« cilin ai pret tĂ« pyetet. Vlera Ă«shtĂ« e barabartĂ« me logaritmin binar tĂ« sekondave.
SaktĂ«sia (saktĂ«sia) — njĂ« numĂ«r tĂ«rĂ« me shenjĂ«, qĂ« pĂ«rfaqĂ«son saktĂ«sinĂ« e orĂ«ve sistemike. Vlera Ă«shtĂ« e barabartĂ« me logaritmin binar tĂ« sekondave.
Shtyrja rrĂ«njore (shtrirja e serverit) – koha qĂ« kĂ«rkohet pĂ«r tĂ« arritur maksimumi i orĂ«ve nĂ« serverin NTP, si numĂ«r sekondash me pikĂ« fikse.
ShkĂ«putja rrĂ«njore (shkalla e shtrirjes sĂ« serverit) — shkalla e shtrirjes sĂ« orĂ«ve tĂ« serverit NTP si numĂ«r sekondash me pikĂ« fikse.
ID referimi (identifikuesi i burimit) – id i orĂ«ve. NĂ«se serveri ka stratum 1, atĂ«herĂ« ref id – emri i orĂ«ve atomike (4 karaktere ASCII). NĂ«se serveri pĂ«rdor njĂ« server tjetĂ«r, atĂ«herĂ« nĂ« ref id shkruhet adresa e kĂ«tij serveri.
KatĂ«r fushat e fundit pĂ«rfaqĂ«sojnĂ« kohĂ«n – 32 bit – pjesa e plotĂ«, 32 bit – pjesa e thyeshĂ«m.
ReferencĂ« — leximet e fundit tĂ« orĂ«ve nĂ« server.
Origjina – koha kur paketa Ă«shtĂ« dĂ«rguar (plotĂ«sohet nga serveri – pĂ«r kĂ«tĂ« mĂ« poshtĂ«).
Marrja – koha e marrjes sĂ« paketĂ«s nga serveri.
DĂ«rgimi – koha e dĂ«rgimit tĂ« paketĂ«s nga serveri te klienti (plotĂ«sohet nga klienti, pĂ«r kĂ«tĂ« gjithashtu mĂ« poshtĂ«).

Dy fushat e fundit nuk do t'i shqyrtojmë.

Le të shkruajmë paketën tonë:

Kodi i paketës

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

    def __init__(self, version_number=2, mode=3, transmit=0):
        # Nevojitet për futjen e sekondës së leap (2 bits)
        self.leap_indicator = 0
        # Versioni i protokollit (3 bits)
        self.version_number = version_number
        # Moda e dërguesit (3 bits)
        self.mode = mode
        # Niveli i leximit të kohës (1 byte)
        self.stratum = 0
        # Intervali mes kërkesave (1 byte)
        self.pool = 0
        # Saktësia (log2) (1 byte)
        self.precision = 0
        # Intervali për arritjen e orës nga serveri NTP (4 bytes)
        self.root_delay = 0
        # Shkallëzimi i orës në serverin NTP (4 bytes)
        self.root_dispersion = 0
        # Treguesi i orëve (4 bytes)
        self.ref_id = 0
        # Koha e fundit e përditësimit në server (8 bytes)
        self.reference = 0
        # Koha e dërgimit të paketës nga makina lokale (8 bytes)
        self.originate = 0
        # Koha e marrjes në server (8 bytes)
        self.receive = 0
        # Koha e dërgimit të përgjigjes nga serveri (8 bytes)
        self.transmit = transmit

Për të dërguar (dhe marrë) një paketë në server, ne duhet të jemi në gjendje ta kthejmë atë në një varg bajtësh.
PĂ«r kĂ«tĂ« (dhe operacionin e kundĂ«rt) le tĂ« shkruajmĂ« dy funksione — pack() dhe unpack():

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

Funksioni 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

PĂ«r ata qĂ« janĂ« tĂ« lenĂ«, si njĂ« aplikacion – kodi qĂ« kthen paketĂ«n nĂ« njĂ« string tĂ« bukur

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)

Dërgimi i paketës në server

Duhet tĂ« dĂ«rgoni njĂ« paketĂ« nĂ« server me fushat e plota Version, Rezhimi dhe DĂ«rgimi. NĂ« DĂ«rgimi duhet tĂ« jepni kohĂ«n aktuale nĂ« makinĂ«n lokale (numri i sekondave qĂ« nga 1 janari 1900), versioni – ndonjĂ« nga 1-4, moda – 3 (reazhi i klientit).

Serveri, pasi merr kĂ«rkesĂ«n, mbush nĂ« paketĂ«n NTP tĂ« gjitha fushat, duke kopjuar nĂ« fushĂ«n Origjina vlĂ«rĂ«simin nga DĂ«rgimi, qĂ« erdhi nĂ« kĂ«rkesĂ«. PĂ«r mua Ă«shtĂ« njĂ« mister pse klienti nuk mund ta mbushĂ« menjĂ«herĂ« vlerĂ«n e tij tĂ« kohĂ«s nĂ« fushĂ«n Origjina. NĂ« pĂ«rfundim, kur paketa kthehet, klienti ka 4 vlera kohe – koha e dĂ«rgimit tĂ« kĂ«rkesĂ«s (Origjina), koha e pranimit tĂ« kĂ«rkesĂ«s nga serveri (Marrja), koha e dĂ«rgimit tĂ« pĂ«rgjigjes nga serveri (DĂ«rgimi) dhe koha e pranimit tĂ« pĂ«rgjigjes nga klienti – Arrive (nuk Ă«shtĂ« nĂ« paketĂ«). Me kĂ«to vlera mund tĂ« vendosim kohĂ«n e saktĂ«.

Kodi për dërgimin dhe marrjen e paketës

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

Procesimi i të dhënave nga serveri

Trajtimi i të dhënave nga serveri është i ngjashëm me veprimet e një gentlemeni anglez nga detyra e vjetër e Raymond M. Smullyan (1978): "Një njeri nuk kishte orë dore, por në shtëpi kishte një orë murale të saktë, të cilën herë pas here e harronte ta ngarkonte. Një ditë, duke e harruar për herë tjetër ta ngarkonte orën, ai shkoi në vizitë te miku i tij, kaloi një mbrëmje atje, dhe kur u kthye në shtëpi, arriti ta vendosë saktësisht orën. Si e arriti këtë, nëse koha e udhëtimit nuk ishte e njohur më parë?" Përgjigjja është: "Duke dalë nga shtëpia, njeriu ngarkon orën dhe e mbart në mendje pozitat e akrepave. Kur arrin te miku dhe largohet nga ai, ai shënon kohën e ardhjes dhe të largimit. Kjo i lejon të mësojë se sa kohë qëndroi në vizitë. Kur kthehet në shtëpi dhe shikon orën, njeriu përcakton kohëzgjatjen e mungesës së tij. Duke e zbritur kohën që kaloi në vizitë nga kjo kohë, ai mëson kohën e shpenzuar për udhëtimin për dhe nga. Duke i shtuar kohës së largimit nga vizita gjysmën e kohës së shpenzuar në udhëtim, ai arrin të kuptojë kohën e ardhjes në shtëpi dhe të rregullojë përkatësisht akrepin e orës së tij."

Gjejmë kohën e punës së serverit mbi kërkesën:

  1. GjejmĂ« kohĂ«n e udhĂ«timit tĂ« paketĂ«s nga klienti te serveri: ((Arrive – Originate) — (Transmit – Receive)) / 2
  2. Gjejmë diferencën mes kohës së klientit dhe serverit:
    Receive — Originate — ((Arrive – Originate) — (Transmit – Receive)) / 2 =
    2 * Receive — 2 * Originate – Arrive + Originate + Transmit – Receive =
    Receive – Originate – Arrive + Transmit

Shtojmë vlerën e marra në kohën lokale dhe gëzohemi për jetën.

Shfaqja e rezultatit

time_different = answer.get_time_different(arrive_time)
result = "Diferenca e kohës: {}nKoha e serverit: {}n{}".format(
    time_different,
    datetime.datetime.fromtimestamp(time.time() + time_different).strftime("%c"),
    answer.to_display())
print(result)

Përdorshme link.

Burimi: habr.com

Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster