Document NTP contracts and verify corrected wall time
This commit is contained in:
1 parent
88bf65e07d
commit
d6eed66926
3 files changed
+55
-24
No files matched your search
+20
-11
@@ -1,25 +1,34 @@
|
||||
# NTP Node Time
|
||||
|
||||
## 1. Назначение
|
||||
Синхронизация времени между узлами uTun через ETCP-канал. Позволяет вычислять смещение часов относительно каждого соседа, обнаруживать дрейф и, если узел сам не синхронизирован с NTP, корректировать свои часы по времени синхронизированного соседа. Используется для точной координации событий между узлами (db_sync, timestamp-ы сообщений в чате).
|
||||
Обмен временем с прямыми ETCP-пирами. Если у узла ещё нет поправки, он принимает время синхронизированного пира с оценкой сетевой задержки. Поправка хранится в `instance->ntp` и применяется getters из `ntp_time.h`; часы ОС не меняются. Отключение интернет-NTP (`ntp.enabled=0`) не отключает этот обмен. Уже синхронизированный узел повторно не принимает поправку от пиров, но интернет-NTP может её обновить.
|
||||
|
||||
## 2. Как пользоваться
|
||||
`ntp_node_time_init()` регистрирует callback на новые ETCP-соединения и callback на приём данных через raw `etcp_bind` (packet ID `ETCP_ID_NTP_TIME = 0x04`, P2P без роутера). При установке нового соединения автоматически вызывается `ntp_node_on_conn_init` — отправка `TIME_SYNC` соседу. При получении `TIME_SYNC` вычисляется per-peer offset, проверяется дрейф, и если мы не синхронизированы а сосед синхронизирован — корректируем свои часы и рассылаем коррекцию всем остальным соседям.
|
||||
В uasync-потоке после создания `instance->connections` вызвать `ntp_node_time_init()`. Он привязывает raw packet ID `ETCP_ID_NTP_TIME = 0x04` (прямой ETCP, без роутера), подписывается на новые соединения и на INIT всех существующих, в том числе pending. На INIT отправляется `TIME_SYNC`; при запуске модуля пакет также отправляется уже инициализированным соединениям. Повторный активный init возвращает 0 без дублирования подписок. При занятом ID или ошибке регистрации возвращается -1; свои частичные регистрации снимаются.
|
||||
|
||||
`ntp_node_sync_peers()` — широковещательная рассылка `TIME_SYNC` всем активным соседям. Вызывается автоматически после успешной NTP-синхронизации из `ntp_time`.
|
||||
`TIME_SYNC` — 10 байт: ID (1), flags (1, bit 0 = synced), время отправителя (int64 Unix мкс в нативном порядке байтов). Синхронизированный отправитель передаёт скорректированное время, остальные — локальное. При первом сообщении сохраняется исходное смещение для диагностики. Если получатель не синхронизирован, а отправитель синхронизирован, применяется:
|
||||
|
||||
`offset_us = local_receive_us - sender_us - mean_link_rtt_us / 2`.
|
||||
|
||||
Среднее RTT — арифметическое по всем элементам `conn->links`, включая нулевые значения и down-линки; `rtt_last` переводится из 0,1 мс в микросекунды. При отсутствии линков задержка считается нулевой. Это приближённая оценка: симметричность пути и точность часов пира не гарантируются.
|
||||
|
||||
После принятия поправки устанавливаются `ntp.synced` и `ntp.last_sync_tb`, затем время рассылается остальным. Синхронизированный получатель отвечает несинхронизированному отправителю. Когда оба синхронизированы, ответа нет — бесконечный обмен не возникает. Полученный entry и его dgram освобождаются при обработке, в том числе при заполненном кеше диагностики.
|
||||
|
||||
`ntp_node_sync_peers()` рассылает время по соединениям с `initialized && links_up`, если модуль активен и есть поправка. Вызывается также после успешной интернет-NTP-синхронизации.
|
||||
|
||||
**Нюансы:**
|
||||
- До 32 соседей (`NTP_NODE_MAX_PEERS`), при переполнении новый не добавляется
|
||||
- Дрейф >2с — WARN, >10с — ERROR. Смещение не применяется автоматически к глобальному offset_us, кроме случая когда свой узел не синхронизирован
|
||||
- Offset считается как `t4 - t_sender_us` (одностороннее смещение, без учёта RTT — предполагается что задержка ETCP мала)
|
||||
- Кеш содержит до 32 диагностических записей, а не ограничивает число соседей. При заполнении вытесняется запись с наименьшим `last_seen_tb` (при равенстве — первая по индексу); исходное смещение новой записи сбрасывается.
|
||||
- Диагностическое смещение — `raw_local_receive_us - sender_us`, без вычитания задержки. Оно отличается от глобальной поправки, принятой с оценкой половины RTT.
|
||||
- При повторном сообщении модуль сравнивает смещение с предыдущим. Модуль разности строго >2 с даёт WARN, строго >10 с — ERROR. Это изменение относительного смещения: оно может включать коррекцию часов отправителя и изменение задержки, а не только дрейф часов.
|
||||
- `destroy` вызывается в uasync-потоке вне callbacks модуля/соединений. Снимает raw binding, подписку новых соединений и свои callbacks со всех текущих соединений, очищает кеш. Глобальную поправку не меняет. Повторный вызов и NULL безопасны.
|
||||
|
||||
## 3. API
|
||||
|
||||
**Структуры:**
|
||||
- `struct NTP_NODE_PEER` — пара `node_id` + `offset_us` для одного соседа
|
||||
- `struct NTP_NODE_TIME` — фиксированный массив `peers[32]` + счётчик `peer_count`
|
||||
- `struct NTP_NODE_PEER` — ID пира, предыдущее диагностическое смещение, наличие исходного значения и время последнего обновления.
|
||||
- `struct NTP_NODE_TIME` — кеш, число занятых записей и флаг активности; контракты полей/функций описаны в `ntp_node_time.h`.
|
||||
|
||||
**Функции:**
|
||||
- `ntp_node_time_init(inst)` — регистрирует bind в `etcp_bind` (ID 0x04) и callback на новые соединения
|
||||
- `ntp_node_time_destroy(inst)` — снимает регистрацию bind и callback, обнуляет peer_count
|
||||
- `ntp_node_sync_peers(inst)` — отправляет `TIME_SYNC` всем активным/initialized соседям (вызывается после NTP-синхронизации)
|
||||
- `ntp_node_time_init(inst)` — 0 при успехе/уже активном модуле, -1 при ошибке; inst и очередь connections обязательны.
|
||||
- `ntp_node_time_destroy(inst)` — снимает регистрации и очищает кеш, сохраняя глобальную поправку.
|
||||
- `ntp_node_sync_peers(inst)` — рассылает время готовым прямым соседям; для NULL/неактивного/несинхронизированного модуля ничего не делает.
|
||||
+20
-10
@@ -1,27 +1,37 @@
|
||||
# NTP Time
|
||||
|
||||
## 1. Назначение
|
||||
SNTP-клиент (RFC 5905) для синхронизации системного времени с внешними NTP-серверами. Модуль вычисляет смещение `offset_us = local_time - ntp_time` (положительное = локальные часы спешат) и позволяет везде в коде использовать `ntp_time_get_us()` вместо `gettimeofday()` для получения скорректированного времени. Без коррекции времени невозможна корректная работа node-to-node синхронизации (`ntp_node_time`) и оценка RTT/jitter в ETCP.
|
||||
Асинхронный клиент интернет-NTP поверх UDP/IPv4. Не меняет часы ОС: хранит поправку `offset_us = local_time - reference_time`, а `ntp_time_get_us()` возвращает `local_time - offset_us`. Положительная поправка означает, что локальные часы спешат. Поправку также может установить `ntp_node_time`; RTT и таймеры ETCP используют монотонное время независимо от неё.
|
||||
|
||||
## 2. Как пользоваться
|
||||
`ntp_time_init()` читает список NTP-серверов из конфига (`global.ntp_servers`) и запускает периодический опрос через uasync-таймер. Опрос — по одному серверу за цикл с round-robin, при неудаче перебирает остальные. Интервал ресинка задаётся через `ntp_resync_interval` (по умолчанию 3600с). Первый запрос — сразу после init. При успешной синхронизации вызывается `ntp_node_sync_peers()` для рассылки времени соседним узлам.
|
||||
Вызвать `ntp_time_init()` в uasync-потоке ядра после создания `instance->config` и `instance->ua`. Исходное состояние нулевое либо после `destroy`; активный повторный `init` возвращает -1, сохраняя прежнее состояние. Обычный `init` сбрасывает прежнюю поправку и тестовые адреса, копирует список серверов и ставит таймер с задержкой 0. Первый цикл выполняется при обработке uasync, не внутри `init`.
|
||||
|
||||
Для тестов: `ntp_time_set_test_addr()` задаёт прямой адрес NTP-сервера (IP:port), минуя DNS-резолвинг.
|
||||
Цикл начинает с `server_current`, обходит каждый сервер не более одного раза и заканчивается при первом корректном ответе. Имена разрешаются асинхронно через A-запросы DNS; используется первый полученный IPv4-адрес. После разрешения адреса — до трёх UDP-запросов с ожиданием по 2 с. Каждая отправка имеет новый timestamp; ответ должен содержать timestamp последней отправки в originate. Немедленная ошибка DNS/сокета/отправки/регистрации прекращает попытку этого сервера и переводит к следующему без рекурсии. Ошибки DNS из callback не сбрасывают остаток обхода.
|
||||
|
||||
После успеха следующий цикл начинается с сервера, следующего за ответившим; после полного отказа исходный индекс сохраняется. Между завершением цикла и следующим запуском — `resync_interval_sec` (по умолчанию 3600 с). При активном опросе допустим интервал 1..`INT_MAX/10000` секунд: таймеры принимают `int` в единицах 0,1 мс. Ошибка создания первого таймера возвращается из `init`; ошибка создания очередного таймера логируется и прекращает автоматические повторы до переинициализации.
|
||||
|
||||
Успех устанавливает `synced`, `offset_us`, `last_sync_tb` и вызывает `ntp_node_sync_peers()`. Полный отказ серверов сохраняет последнюю поправку. `reachable` отражает успех последнего завершённого интернет-NTP-цикла; изначально он оптимистично равен 1 даже при отключённом опросе. Его изменение обновляет адреса узла через `topo_node_update_my_addresses()`; это косвенный признак для классификации сокетов, а не гарантия доступа в интернет.
|
||||
|
||||
Тестовые подмены устанавливаются после `init`, до первого `poll`:
|
||||
- `ntp_time_set_test_addr(ntp, ip, port)` — IPv4-литерал и порт единственного сервера цикла, без DNS и перехода к конфигурационным серверам. `port=0` отключает подмену. Конфигурационный список должен оставаться непустым.
|
||||
- `ntp_time_set_test_dns(ntp, ip, dns_port, ntp_port)` — IPv4 DNS-сервер и порт NTP для конфигурационного списка (`ntp_port=0` означает 123). `test_addr` имеет приоритет. `dns_port=0` отключает подмену DNS.
|
||||
|
||||
**Нюансы:**
|
||||
- Если `ntp_enabled=0` или серверов нет, модуль не опрашивает, `ntp_time_get_us()` возвращает локальное время
|
||||
- Если синхронизации не было, то же самое — возвращается `gettimeofday()` без коррекции
|
||||
- SNTP (simplified NTP) — использует только один запрос-ответ, без сложной фильтрации
|
||||
- `ntp_enabled=0` или пустой список отключают только интернет-опрос; коррекция от пиров остаётся возможной.
|
||||
- До первой коррекции getters возвращают локальное время; `synced` означает наличие поправки, а не её свежесть.
|
||||
- Нет фильтрации серии измерений или гарантии точного времени; сетевые задержки влияют на оценку.
|
||||
- `destroy` вызывается в uasync-потоке вне callbacks модуля, до освобождения ua. Отменяет DNS, сокет и таймеры, освобождает серверы и отключает опрос, сохраняя `synced`, `offset_us`, `last_sync_tb`, `reachable`.
|
||||
|
||||
## 3. API
|
||||
|
||||
**Структуры:**
|
||||
- `struct NTP_TIME` — состояние синхронизации: флаг synced, offset_us, last_sync_tb, список серверов, текущий индекс и таймер
|
||||
- `struct NTP_TIME` — состояние коррекции и интернет-опроса; поля и контракты функций описаны в `ntp_time.h`.
|
||||
|
||||
**Функции:**
|
||||
- `ntp_time_init(instance)` — читает конфиг, выделяет массив серверов, запускает первый sync-таймер
|
||||
- `ntp_time_destroy(instance)` — отменяет таймер, освобождает память серверов
|
||||
- `ntp_time_init(instance)` — 0 при успехе (также при отключённом опросе), -1 при ошибке; новые ресурсы при ошибке освобождаются.
|
||||
- `ntp_time_destroy(instance)` — отменяет опрос, сохраняет поправку; NULL и повторный вызов допустимы.
|
||||
- `ntp_time_set_test_addr(ntp, ip, port)` — задать NTP-сервер напрямую (без DNS) для тестов
|
||||
- `ntp_time_get_us(instance)` — скорректированное время в микросекундах (если synced), иначе `gettimeofday()`
|
||||
- `ntp_time_get_seconds(instance)` — скорректированное время в секундах (time_t)
|
||||
- `ntp_time_is_synced(instance)` — true если была успешная синхронизация
|
||||
- `ntp_time_is_synced(instance)` — ненулевое значение при наличии поправки от NTP/пира, для NULL — 0.
|
||||
- Getters для NULL возвращают локальное время; секунды вычисляются целочисленным делением микросекунд с приведением к `time_t`.
|
||||
+15
-3
@@ -5,7 +5,7 @@
|
||||
* Сценарий 1 (direct): фейковый NTP-сервер, синхронизация по test_addr.
|
||||
* Сценарий 2 (hostname): фейковый DNS-сервер резолвит hostname → 127.0.0.1,
|
||||
* затем NTP опрашивает фейковый NTP-сервер (async_dns → неблокирующий запрос).
|
||||
* Оба сценария проверяют флаг synced и точность offset.
|
||||
* Оба сценария проверяют флаг synced, знак offset и итоговое скорректированное время getters.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -221,17 +221,29 @@ static int verify_synced(struct UTUN_INSTANCE* inst, struct UASYNC* ua, const ch
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: %s did not sync within %ds", label, TEST_TIMEOUT_SEC);
|
||||
return -1;
|
||||
}
|
||||
int64_t diff = inst->ntp.offset_us - TEST_KNOWN_OFFSET_US;
|
||||
int64_t diff = inst->ntp.offset_us + TEST_KNOWN_OFFSET_US;
|
||||
if (diff < 0) diff = -diff;
|
||||
if (diff > OFFSET_TOLERANCE_US) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: %s offset %lldus too far from expected %lldus (diff=%lldus)",
|
||||
label, (long long)inst->ntp.offset_us, (long long)TEST_KNOWN_OFFSET_US, (long long)diff);
|
||||
label, (long long)inst->ntp.offset_us, (long long)-TEST_KNOWN_OFFSET_US, (long long)diff);
|
||||
return -1;
|
||||
}
|
||||
if (!ntp_time_is_synced(inst)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: %s ntp_time_is_synced returned 0", label);
|
||||
return -1;
|
||||
}
|
||||
struct timeval before, after;
|
||||
utun_gettimeofday(&before, NULL);
|
||||
int64_t corrected_us = ntp_time_get_us(inst);
|
||||
time_t corrected_sec = ntp_time_get_seconds(inst);
|
||||
utun_gettimeofday(&after, NULL);
|
||||
int64_t lower = (int64_t)before.tv_sec * 1000000 + before.tv_usec + TEST_KNOWN_OFFSET_US - OFFSET_TOLERANCE_US;
|
||||
int64_t upper = (int64_t)after.tv_sec * 1000000 + after.tv_usec + TEST_KNOWN_OFFSET_US + OFFSET_TOLERANCE_US;
|
||||
if (corrected_us < lower || corrected_us > upper || corrected_sec < lower / 1000000 || corrected_sec > upper / 1000000) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: %s corrected time outside server clock bounds: us=%lld sec=%lld range=%lld..%lld",
|
||||
label, (long long)corrected_us, (long long)corrected_sec, (long long)lower, (long long)upper);
|
||||
return -1;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "PASS: %s offset=%lldus", label, (long long)inst->ntp.offset_us);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user