Basit bir NTP istemcisi yazmak

Merhaba habrauserler. Bugün kendi basit NTP istemcinizi nasıl yazacağınız hakkında konuşmak istiyorum. Temel olarak konuşma, paketin yapısına ve NTP sunucusundan gelen yanıtın nasıl işlendiğine dönecektir. Kod python ile yazılacak, çünkü bence bu tür şeyler için daha iyi bir dil yok. Uzmanlar, kodun ntplib koduyla benzerliğine dikkat edecekler - ondan "ilham aldım".

Peki zaten NTP nedir? NTP, zaman sunucuları ile iletişim kurmak için kullanılan bir protokoldür. Bu protokol birçok modern makinede kullanılmaktadır. Örneğin, pencerelerdeki w32tm hizmeti.

NTP protokolünün toplamda 5 versiyonu bulunmaktadır. İlk sürüm 0 (1985, RFC958) şu anda eski olarak kabul edilmektedir. Daha yenileri şu anda kullanılmaktadır, 1. (1988, RFC1059), 2. (1989, RFC1119), 3. (1992, RFC1305) ve 4. (1996, RFC2030). 1-4 sürümleri birbiriyle uyumludur, yalnızca sunucuların algoritmalarında farklılık gösterirler.

Paket Biçimi

Basit bir NTP istemcisi yazmak

sıçrama göstergesi (düzeltme göstergesi), artık saniye uyarısını gösteren bir sayıdır. Anlam:

  • 0 - düzeltme yok
  • 1 - günün son dakikası 61 saniye içerir
  • 2 - günün son dakikası 59 saniye içerir
  • 3 - sunucu hatası (senkronize olmayan zaman)

Sürüm numarası (sürüm numarası) – NTP protokolü sürüm numarası (1-4).

Moda (mod) — paket göndericinin çalışma modu. 0 ile 7 arasındaki değer, en yaygın:

  • 3 - müşteri
  • 4 - sunucu
  • 5 - yayın modu

tabaka (katman düzeyi) - sunucu ile referans saat arasındaki ara katmanların sayısı (1 - sunucu, verileri doğrudan referans saatten alır, 2 - sunucu, 1. düzeydeki sunucudan veri alır, vb.).
havuz ardışık mesajlar arasındaki maksimum aralığı temsil eden işaretli bir tamsayıdır. NTP istemcisi burada, sunucuyu yoklamayı beklediği aralığı belirtir ve NTP sunucusu, sorgulanmasını beklediği aralığı belirtir. Değer, saniyenin ikili logaritmasına eşittir.
Hassas (kesinlik), sistem saatinin doğruluğunu temsil eden işaretli bir tamsayıdır. Değer, saniyenin ikili logaritmasına eşittir.
kök gecikmesi (sunucu gecikmesi), saatin sabit noktalı saniye sayısı olarak NTP sunucusuna ulaşması için geçen süredir.
kök dağılımı (sunucu dağılımı) - NTP sunucu saatinin sabit noktalı saniye sayısı olarak dağılımı.
referans kimliği (kaynak kimliği) – izleme kimliği. Sunucuda katman 1 varsa, ref id atomik saatin adıdır (4 ASCII karakteri). Sunucu başka bir sunucu kullanıyorsa, ref kimliği bu sunucunun adresini içerir.
Son 4 alan zamandır - 32 bit - tamsayı kısmı, 32 bit - kesirli kısım.
Referans - sunucudaki en son saat.
Kökeni – paketin gönderildiği saat (sunucu tarafından dolduruldu – aşağıda daha fazlası).
Teslim almak – paketin sunucu tarafından alındığı zaman.
Iletmek – paketin sunucudan istemciye gönderildiği zaman (istemci tarafından doldurulur, aşağıda daha fazlası).

Son iki alan dikkate alınmayacaktır.

Paketimizi yazalım:

paket kodu

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

Sunucuya bir paket göndermek (ve almak) için, onu bir bayt dizisine çevirebilmeliyiz.
Bu (ve ters) işlem için iki işlev yazacağız - pack() ve unpack():

paket işlevi

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

paketi açma işlevi

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

Tembel insanlar için, bir uygulama olarak - paketi güzel bir diziye dönüştüren kod

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)

Sunucuya bir paket gönderme

Doldurulmuş alanları olan bir paketi sunucuya gönderin Sürümü, Moda и Iletmek. Iletmek yerel makinede geçerli saati (1 Ocak 1900'den bu yana geçen saniye sayısı), sürüm - 1-4'ten herhangi biri, mod - 3 (istemci modu) belirtmelisiniz.

İsteği alan sunucu, alana kopyalayarak NTP paketindeki tüm alanları doldurur. Kökeni gelen değer Iletmek, istek geldi. Müşterinin sahada geçirdiği zamanın değerini neden hemen dolduramadığı benim için bir muamma. Kökeni. Sonuç olarak, paket geri geldiğinde, istemcinin 4 zaman değeri vardır - isteğin gönderildiği saat (Kökeni), sunucunun isteği aldığı saat (Teslim almak), sunucunun yanıtı gönderdiği saat (Iletmek) ve müşteri tarafından bir yanıtın alındığı zaman - varmak (pakette değil). Bu değerler ile doğru zamanı ayarlayabiliriz.

Paket gönderme ve alma kodu

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

Sunucudan veri işleme

Verilerin sunucudan işlenmesi, İngiliz beyefendinin Raymond M. Smallian'ın (1978) eski problemindeki eylemlerine benzer: “Bir kişinin kol saati yoktu, ancak evde doğru bir duvar saati vardı. bazen rüzgarı unuttum. Bir gün saati yeniden çalıştırmayı unutarak arkadaşını ziyarete gitti, akşamı onunla geçirdi ve eve döndüğünde saati doğru kurmayı başardı. Seyahat süresi önceden bilinmiyorsa bunu nasıl başardı? Cevap şudur: “Evden çıkan kişi saati kurar ve ellerin konumunu hatırlar. Bir arkadaşına gelip misafirlerden ayrılarak geliş ve gidiş saatini not eder. Bu, ne kadar süre uzakta olduğunu öğrenmesini sağlar. Eve dönen ve saate bakan kişi, yokluğunun süresini belirler. Kişi bu süreden ziyarette geçirdiği süreyi çıkardığında, kişi orada ve dönüşte yolda geçirdiği süreyi bulur. Yolda geçirilen zamanın yarısını misafirlerin ayrılma saatine ekleyerek, eve geliş saatini öğrenme ve saatinin akreplerini buna göre ayarlama fırsatı yakalar.

Sunucunun istek üzerinde çalıştığı zamanı bulun:

  1. İstemciden sunucuya paket seyahat süresini bulma: ((Var - Oluştur) - (İlet - Al)) / 2
  2. İstemci ve sunucu zamanı arasındaki farkı bulun:
    Al - Oluştur - ((Var - Oluştur) - (İlet - Al)) / 2 =
    2 * Al - 2 * Oluştur - Gel + Oluştur + İlet - Al =
    Al - Başlat - Gel + İlet

Alınan değeri yerel saate katıyor ve hayatın tadını çıkarıyoruz.

Sonuç Çıktısı

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)

Kullanışlı bağlantı.

Kaynak: habr.com

Yorum ekle