NTP: как работает протокол сетевого времени и как настроить синхронизацию

NTP: как работает протокол сетевого времени и как настроить синхронизацию Полезное

NTP (Network Time Protocol) — это протокол, по которому компьютер по сети узнает точное время у серверов и плавно подстраивает свои часы к UTC. Он работает поверх UDP, порт 123, и умеет учитывать задержку сети: клиент не просто спрашивает «который час», а измеряет, сколько шел ответ, и вычитает это из поправки.

Ниже разберем, как NTP считает поправку по четырем меткам времени (с рабочим примером на Python), что такое stratum, чем NTPv4 отличается от SNTP и NTS, как настроить chrony в Linux и w32time в Windows и как не превратить свой сервер времени в усилитель DDoS.

Зачем синхронизировать время

Часы любого компьютера дрейфуют: кварцевый генератор может уходить на секунды в сутки в зависимости от температуры и экземпляра. Расхождение в несколько секунд или минут ломает вполне конкретные вещи:

  • Kerberos и вход в домен Active Directory: по умолчанию допустимое расхождение часов — 5 минут, дальше аутентификация отказывает;
  • TLS: сертификат «еще не действителен» или «уже истек», если часы клиента ушли;
  • логи и трассировка: события с разных серверов нельзя сопоставить по времени;
  • распределенные системы, репликация и планировщики задач, где порядок событий важен.

Словарь: offset, delay, stratum

Эти три термина постоянно путают, поэтому сразу разведем их:

Термин Что это Пример
offset (смещение) насколько часы клиента отличаются от часов сервера +2.5 s: сервер впереди на 2,5 секунды
delay (задержка) время пути запроса и ответа по сети туда и обратно, без обработки на сервере 40 ms
stratum (уровень) «расстояние» сервера от эталонных часов в иерархии 1 — сервер с GPS-приемником, 2 — его клиент
jitter (разброс) насколько скачут последовательные измерения offset 0.3 ms

Как NTP считает поправку: 4 метки времени

Клиент и сервер обмениваются одним пакетом в каждую сторону, и в этом обмене появляются четыре метки:

  1. T1 — клиент отправил запрос (по часам клиента);
  2. T2 — сервер получил запрос (по часам сервера);
  3. T3 — сервер отправил ответ (по часам сервера);
  4. T4 — клиент получил ответ (по часам клиента).

Из них клиент вычисляет:

  • offset = ((T2 — T1) + (T3 — T4)) / 2
  • delay = (T4 — T1) — (T3 — T2)

Прогоним руками. Часы сервера впереди на 2,5 с, путь в каждую сторону — 20 мс, сервер обрабатывает запрос 1 мс. T1 = 10.000, T2 = 12.520, T3 = 12.521, T4 = 10.041. Тогда offset = (2.520 + 2.480) / 2 = 2.5 с, delay = 0.041 — 0.001 = 0.040 с. Поправка найдена точно.

Граница модели: формула предполагает, что путь туда и обратно занимает одинаковое время. Если запрос идет 30 мс, а ответ 10 мс (T2 = 12.530, T3 = 12.531), offset получится 2.51 с — ошибка ровно в половину асимметрии, (30 — 10) / 2 = 10 мс. Измерить асимметрию изнутри одного обмена нельзя, поэтому NTP берет несколько серверов и много замеров и доверяет тем, у кого delay меньше.

Минимальный SNTP-клиент на Python

Скрипт ниже запускает на localhost учебный «сервер» с часами, которые спешат на 2,5 с, отправляет ему 48-байтный пакет NTPv4 и вычисляет offset и delay по тем же формулам. Сеть и права root не нужны: сервер слушает свободный порт, а не 123.

import socket
import struct
import threading
import time

NTP_DELTA = 2208988800  # секунды между 1900-01-01 и 1970-01-01
SERVER_SKEW = 2.5       # часы учебного сервера спешат на 2,5 с


def to_ntp(t):
    """Unix-время (float) -> 64-битная метка NTP: секунды + доля."""
    sec = int(t) + NTP_DELTA
    frac = int((t - int(t)) * 2**32)
    return sec, frac


def from_ntp(sec, frac):
    return sec - NTP_DELTA + frac / 2**32


def server(sock):
    data, addr = sock.recvfrom(48)
    t2 = time.time() + SERVER_SKEW              # T2: прием запроса
    t1_sec, t1_frac = struct.unpack("!II", data[40:48])
    t3 = time.time() + SERVER_SKEW              # T3: отправка ответа
    reply = struct.pack(
        "!BBbb11I",
        (0 << 6) | (4 << 3) | 4,  # LI=0, VN=4, Mode=4 (server)
        2, 6, -20,                # stratum, poll, precision
        0, 0, 0,                  # root delay, root dispersion, refid
        *to_ntp(t3),              # reference timestamp
        t1_sec, t1_frac,          # origin = T1 клиента
        *to_ntp(t2),              # receive = T2
        *to_ntp(t3),              # transmit = T3
    )
    sock.sendto(reply, addr)


srv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
srv.bind(("127.0.0.1", 0))  # свободный порт вместо 123: не нужен root
threading.Thread(target=server, args=(srv,), daemon=True).start()

cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
cli.settimeout(2)
t1 = time.time()
request = struct.pack("!B39x", (0 << 6) | (4 << 3) | 3)  # Mode=3 (client)
t1_ntp = to_ntp(t1)
request += struct.pack("!II", *t1_ntp)  # transmit = T1
cli.sendto(request, srv.getsockname())
reply, _ = cli.recvfrom(48)
t4 = time.time()
if len(reply) < 48:
    raise ValueError("слишком короткий пакет")

fields = struct.unpack("!BBbb11I", reply)
li_vn_mode, stratum = fields[0], fields[1]
if li_vn_mode & 7 != 4 or stratum == 0 or li_vn_mode >> 6 == 3:
    # не ответ сервера, Kiss-o'-Death (stratum 0) или LI=3: сервер не синхронизирован
    raise ValueError("ответ нельзя использовать для времени")
if tuple(fields[9:11]) != t1_ntp:  # origin должен совпасть с нашим T1
    raise ValueError("чужой или поддельный ответ")
t2 = from_ntp(fields[11], fields[12])
t3 = from_ntp(fields[13], fields[14])

offset = ((t2 - t1) + (t3 - t4)) / 2
delay = (t4 - t1) - (t3 - t2)
print(f"version={li_vn_mode >> 3 & 7} mode={li_vn_mode & 7} stratum={stratum}")
print(f"offset = {offset:+.3f} s")
print(f"delay  = {delay * 1000:.3f} ms")

Вывод на Python 3.14 (delay на вашей машине будет другим, это доли миллисекунды на localhost):

version=4 mode=4 stratum=2
offset = +2.500 s
delay  = 0.054 ms

Что важно в коде:

  • первый байт пакета упаковывает три поля: LI (предупреждение о секунде координации), VN (версия, 4) и Mode (3 — клиент, 4 — сервер);
  • метка времени — 64 бита: 32 бита секунд с 1 января 1900 года и 32 бита доли секунды;
  • ответ с неверным режимом, stratum 0 (Kiss-o’-Death — сервер просит снизить частоту запросов или не обращаться к нему) или LI=3 для времени не используют;
  • сервер копирует T1 клиента в поле origin, и клиент сверяет его со своим. Это отсекает случайные и «слепые» подложные ответы, но не является аутентификацией: атакующий, который видит трафик, подделает и origin. Защиту дает NTS (ниже).

Это SNTP-логика одного замера. Настоящий демон делает замеры регулярно, фильтрует выбросы и подстраивает частоту часов, а не просто прибавляет offset.

Типичная ошибка: забыть про эпоху 1900 года

Секунды в пакете NTP отсчитываются от 1900 года, а Unix-время — от 1970. Если прочитать их как Unix-время:

from datetime import datetime, timezone

ntp_seconds = 4_000_000_000  # секунды из поля метки NTP

wrong = datetime.fromtimestamp(ntp_seconds, timezone.utc)
right = datetime.fromtimestamp(ntp_seconds - 2208988800, timezone.utc)
print("как Unix-время:", wrong)
print("с поправкой   :", right)
как Unix-время: 2096-10-02 07:06:40+00:00
с поправкой   : 2026-10-03 07:06:40+00:00

Ошибка — 2208988800 секунд, то есть 70 лет с учетом 17 високосных дней. Исправление — вычитать константу 2208988800. Отсюда же «проблема 2036 года»: 32-битный счетчик секунд NTP переполнится в феврале 2036-го, и реализации должны корректно обрабатывать смену эры.

Stratum и как устроена иерархия

  • Stratum 0 — эталонные часы: GPS/ГЛОНАСС-приемник, атомные часы. В сети NTP они не участвуют, подключаются к серверу напрямую.
  • Stratum 1 — сервер, к которому подключены эталонные часы.
  • Stratum 2, 3… — серверы, синхронизированные с уровнем выше; каждый переход добавляет единицу.
  • Максимум — 15. Значение 16 означает «не синхронизирован». Поле в пакете восьмибитное, но значения выше 16 зарезервированы, так что «256 уровней» в старых пересказах — ошибка.

Stratum — это не показатель точности. Сервер stratum 2 в соседней стойке нередко дает время точнее, чем stratum 1 через океан, потому что меньше и стабильнее delay.

Из нескольких источников NTP выбирает «правдивых» алгоритмом пересечений (развитие алгоритма Марзулло): интервалы [offset — погрешность, offset + погрешность] должны перекрываться у большинства. Поэтому рекомендуют от четырех источников: при двух непонятно, кто врет, а при трех один сбойный уже выявляется, но отказ еще одного оставляет систему без запаса.

NTPv4, SNTP, NTS и PTP: что выбрать

Вариант Что это Когда брать
NTPv4 (RFC 5905) полный протокол: много источников, фильтрация, дисциплина частоты часов серверы, рабочие станции, свой сервер времени для сети
SNTP тот же формат пакета, но упрощенный клиент, обычно один источник без сложной фильтрации встраиваемые устройства, простые клиенты; например, systemd-timesyncd
NTS (RFC 8915) Network Time Security: согласование ключей по TLS (TCP 4460), дальше NTP-пакеты с проверкой подлинности когда важна защита от подмены времени и сервер NTS поддерживает
PTP (IEEE 1588) другой протокол с аппаратными метками времени в сетевых картах и коммутаторах нужна точность в микросекунды и лучше: биржи, телеком, промышленность

Типичная точность NTP через интернет — единицы или десятки миллисекунд, в хорошей локальной сети — доли миллисекунды. Это ориентир, а не гарантия: она зависит от асимметрии маршрута, нагрузки и качества источников.

Настройка chrony в Linux

chrony — основной NTP-демон в RHEL-семействе и во многих других дистрибутивах; классический ntpd из пакета ntp постепенно уходит (где-то его заменяет ntpsec). Пути ниже — для Debian/Ubuntu (/etc/chrony/chrony.conf); в RHEL/Fedora файл лежит в /etc/chrony.conf, служба называется chronyd.

В Ubuntu с версии 25.10 chrony стоит по умолчанию на новых установках (при обновлении с 25.04 может остаться systemd-timesyncd). Штатные источники там описаны в /etc/chrony/sources.d/ubuntu-ntp-pools.sources и работают через NTS, поэтому кроме UDP 123 нужен исходящий TCP 4460. Если сеть его не пропускает, chrony с конфигурацией по умолчанию часы не подстроит.

Сначала сохраните исходный конфиг под уникальным именем и запомните путь — это и есть точка отката:

sudo apt install chrony
CONF=/etc/chrony/chrony.conf
BACKUP="$CONF.bak-$(date +%F-%H%M%S)"
test ! -e "$BACKUP" && sudo cp -p "$CONF" "$BACKUP" && echo "копия: $BACKUP"

Минимальная клиентская конфигурация:

# /etc/chrony/chrony.conf (фрагмент)
pool ru.pool.ntp.org iburst maxsources 4
driftfile /var/lib/chrony/chrony.drift
makestep 1.0 3
rtcsync
  • pool берет несколько серверов из DNS-пула; iburst на старте посылает серию быстрых запросов, чтобы синхронизироваться за секунды, а не минуты;
  • driftfile хранит измеренную скорость ухода часов, чтобы после перезагрузки не начинать с нуля;
  • makestep 1.0 3 разрешает «прыжок» часов, если расхождение больше 1 с, но только в первых трех обновлениях; дальше время подстраивается плавно, без скачков назад;
  • rtcsync периодически копирует системное время в аппаратные часы.

Применение и проверка:

sudo systemctl restart chrony
chronyc sources -v
chronyc tracking

В chronyc sources символ ^* отмечает выбранный источник, ^+ — пригодные для усреднения, ^- — отброшенные, ^? — недоступные или непроверенные. В tracking смотрите строку System time (текущее смещение) и Leap status (должно быть Normal). Откат: sudo cp -p "$BACKUP" "$CONF", затем снова restart и chronyc tracking.

Раздавать время в локальную сеть

По умолчанию chrony работает только клиентом. Чтобы машина стала сервером для своей подсети, добавьте allow с конкретной сетью, а не для всех:

# сервер времени для офисной подсети (адрес - пример)
allow 192.168.10.0/24
# в изолированной сети без интернета: раздавать свои часы, пометив их stratum 10
local stratum 10

local stratum 10 — честная замена старому совету server 127.127.1.0 из ntpd: без внешних источников ваш сервер не становится точным, он лишь дает сети общее время. Клиенты при этом будут одинаково неправы, но согласованы между собой.

Настройка w32time в Windows

Компьютеры в домене Active Directory получают время по иерархии домена, и вручную их трогать не нужно: внешние источники настраивают только на контроллере с ролью PDC-эмулятора корневого домена леса. Для отдельной машины или этого контроллера (команды в cmd от администратора):

w32tm /config /manualpeerlist:"0.ru.pool.ntp.org,0x8 1.ru.pool.ntp.org,0x8" /syncfromflags:manual /update
w32tm /resync
w32tm /query /status
w32tm /query /peers

Флаг 0x8 — запросы в клиентском режиме. /query /status показывает источник, stratum и время последней синхронизации. Если /resync пишет, что данные о времени недоступны, проверьте, открыт ли исходящий UDP 123 на межсетевом экране.

Безопасность NTP-сервера

Amplification. Старые версии ntpd отвечали на служебный запрос monlist списком последних клиентов — ответ в сотни раз больше запроса. Атакующий подделывал адрес жертвы, и открытые NTP-серверы заливали ее трафиком (CVE-2013-5211). Что делать:

  • не держать публично открытый сервер, если он не нужен; раздавать время только своим подсетям (allow в chrony);
  • в ntpd — актуальная версия и ограничения restrict default kod limited nomodify notrap nopeer noquery, где noquery закрывает служебные запросы, а limited и kod режут частые запросы;
  • на межсетевом экране пропускать UDP 123 внутрь только из доверенных сетей.

Подмена времени. Обычный NTP не аутентифицирует сервер: атакующий на маршруте может сдвинуть часы клиента и, например, «оживить» отозванный сертификат. Защита — NTS там, где сервер его поддерживает (server <имя> iburst nts в chrony 4.x), несколько независимых источников и makestep только на старте, чтобы время нельзя было резко сдвинуть в работе. Это снижает риск, но не делает синхронизацию неуязвимой: доступность UDP-трафика все равно можно нарушить.

Если не получилось

  • chronyc sources пуст или все ^? — закрыт исходящий UDP 123 (а для NTS-источников, как в Ubuntu 25.10+, еще и TCP 4460) или не резолвится DNS пула; проверьте межсетевой экран, chronyc -N authdata для NTS и getent hosts ru.pool.ntp.org.
  • Время не прыгает к правильному — смещение больше порога makestep, а лимит обновлений исчерпан; однократно выполните sudo chronyc makestep.
  • Две службы спорят за часы — одновременно работают systemd-timesyncd и chrony; timedatectl status покажет активную, лишнюю отключите.
  • Виртуальная машина уходит после паузы — гипервизор тоже синхронизирует время гостя; оставьте один механизм.

Выводы

  • NTP вычисляет offset и delay по четырем меткам времени; точность ограничена асимметрией маршрута, поэтому нужны несколько источников, лучше от четырех.
  • Stratum — расстояние от эталона (1-15, 16 = не синхронизирован), а не мера точности.
  • SNTP — упрощенный клиент того же протокола, NTS добавляет проверку подлинности, PTP нужен для микросекундной точности.
  • В Linux практичный выбор — chrony (pool ... iburst, makestep на старте, allow только для своих сетей), в Windows — w32tm, в домене AD — иерархия от PDC-эмулятора.
  • Публичный NTP-сервер без ограничений — риск amplification-атак; закрывайте служебные запросы и фильтруйте UDP 123.

Где применяется / связь с практикой

Освойте тему на практике

Синхронизация времени — одна из базовых сетевых служб, которую сетевой инженер настраивает на маршрутизаторах, коммутаторах и серверах наравне с DNS и DHCP: без нее не сходятся логи, отваливаются аутентификация и сертификаты. Сетевые протоколы, UDP-сервисы и их защиту системно разбирают на курсе «Сетевой инженер. Базовый уровень». Отдельные темы по сетям и администрированию можно посмотреть на открытых уроках Otus.

FAQ

Какой порт использует NTP?
UDP 123 для самих запросов времени. NTS дополнительно использует TCP 4460 для согласования ключей.

Чем NTP отличается от команды date, которой можно выставить время?
date один раз ставит часы по вашему вводу. NTP-демон постоянно измеряет смещение и подстраивает частоту хода часов, поэтому время остается точным, а не только в момент установки.

Что делать с секундой координации?
Демоны получают о ней предупреждение через поле LI и отрабатывают ее сами. Некоторые крупные провайдеры вместо вставки секунды «размазывают» ее на сутки (у Google — линейно с полудня до полудня UTC) — смешивать такие источники с обычными не рекомендуют.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)