Cover NTP correction, retries, faults and peer lifecycle

This commit is contained in:
evgeny committed 2026-10-21 20:15:17 +02:00
1 parent 13c1831baa
commit 88bf65e07d
2 files changed
+494

No files matched your search

+11
View File
@@ -96,6 +96,7 @@ check_PROGRAMS = \
if OS_LINUX
check_PROGRAMS += \
test_ntp_regressions \
test_firewall \
test_mem \
test_config_updater \
@@ -573,6 +574,16 @@ test_ntp_SOURCES = test_ntp.c
test_ntp_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_ntp_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
if OS_LINUX
test_ntp_regressions_SOURCES = test_ntp_regressions.c
test_ntp_regressions_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_ntp_regressions_LDFLAGS = -Wl,--wrap=gettimeofday -Wl,--wrap=uasync_set_timeout -Wl,--wrap=uasync_cancel_timeout \
-Wl,--wrap=socket_create_udp -Wl,--wrap=socket_set_nonblocking -Wl,--wrap=socket_close_wrapper \
-Wl,--wrap=socket_sendto -Wl,--wrap=socket_recvfrom -Wl,--wrap=uasync_add_socket_t -Wl,--wrap=uasync_remove_socket_t \
-Wl,--wrap=adns_resolve -Wl,--wrap=adns_cancel -Wl,--wrap=topo_node_update_my_addresses -Wl,--wrap=etcp_send \
-Wl,--wrap=u_malloc_impl -Wl,--wrap=u_calloc_impl -Wl,--wrap=u_strdup_impl
endif
test_async_dns_SOURCES = test_async_dns.c
test_async_dns_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_async_dns_LDADD = $(COMMON_LIBS)
+483
View File
@@ -0,0 +1,483 @@
/* Расчёт времени и отказы NTP: реальные модули/API, управляемые часы, DNS, UDP и таймеры без сети/ожиданий. */
#define _GNU_SOURCE
#include "../src/utun_instance.h"
#include "../src/ntp_time.h"
#include "../src/ntp_node_time.h"
#include "../src/transport_layer/etcp_api.h"
#include "../src/transport_layer/etcp.h"
#include "../lib/async_dns.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include <assert.h>
#include <limits.h>
#include <string.h>
#define FAKE_SOCKET 12345678
#define WALL_START_US 1700000000000000LL
struct timer {
int active, delay_tb;
timeout_callback_t callback;
void* arg;
};
static struct timer timers[4];
static int64_t wall_us;
static int socket_open, socket_registered, creates, sends, peer_sends, address_updates, dns_calls, dns_cancels;
static int fail_create, fail_nonblock, fail_send, fail_register, fail_dns;
static const char* fail_timer;
static int fail_alloc_at, alloc_attempts;
static socket_t_callback_t read_callback;
static void* read_arg;
static adns_done_cb dns_callback;
static void* dns_arg;
static uint8_t request[48], reply[48];
static int reply_length;
extern int __real_socket_close_wrapper(socket_t);
extern void* __real_u_malloc_impl(uint32_t, const char*);
extern void* __real_u_calloc_impl(uint32_t, uint32_t, const char*);
extern char* __real_u_strdup_impl(const char*, const char*);
static int allocation_fault(void) { return fail_alloc_at && ++alloc_attempts == fail_alloc_at; }
void* __wrap_u_malloc_impl(uint32_t size, const char* where) {
return allocation_fault() ? NULL : __real_u_malloc_impl(size, where);
}
void* __wrap_u_calloc_impl(uint32_t count, uint32_t size, const char* where) {
return allocation_fault() ? NULL : __real_u_calloc_impl(count, size, where);
}
char* __wrap_u_strdup_impl(const char* value, const char* where) {
return allocation_fault() ? NULL : __real_u_strdup_impl(value, where);
}
int __wrap_gettimeofday(struct timeval* tv, void* tz) {
(void)tz;
tv->tv_sec = wall_us / 1000000;
tv->tv_usec = wall_us % 1000000;
return 0;
}
void* __wrap_uasync_set_timeout(struct UASYNC* ua, int delay, void* arg, timeout_callback_t cb, const char* name) {
assert(ua && cb && delay >= 0);
assert(!strcmp(name, "ntp_sync") || !strcmp(name, "ntp_q"));
if (fail_timer && !strcmp(fail_timer, name)) { fail_timer = NULL; return NULL; }
for (size_t i = 0; i < sizeof(timers) / sizeof(timers[0]); i++) {
if (timers[i].active) continue;
timers[i] = (struct timer){.active = 1, .delay_tb = delay, .callback = cb, .arg = arg};
return &timers[i];
}
assert(!"too many live timers");
return NULL;
}
err_t __wrap_uasync_cancel_timeout(struct UASYNC* ua, void* handle) {
assert(ua && handle);
struct timer* t = handle;
assert(t >= timers && t < timers + 4 && t->active);
t->active = 0;
return ERR_OK;
}
static void fire_timer(void* handle) {
struct timer* t = handle;
assert(t && t->active);
struct timer saved = *t;
t->active = 0;
wall_us += (int64_t)saved.delay_tb * 100;
saved.callback(saved.arg);
}
static void fire_query_timer(void) {
for (int i = 0; i < 4; i++) {
if (timers[i].active && timers[i].delay_tb == 20000) { fire_timer(&timers[i]); return; }
}
assert(!"no query timer");
}
socket_t __wrap_socket_create_udp(int family) {
assert(family == AF_INET && !socket_open);
creates++;
if (fail_create) { errno = EMFILE; return SOCKET_INVALID; }
socket_open = 1;
return FAKE_SOCKET;
}
int __wrap_socket_set_nonblocking(socket_t sock) {
assert(sock == FAKE_SOCKET && socket_open);
errno = EIO;
return fail_nonblock ? -1 : 0;
}
int __wrap_socket_close_wrapper(socket_t sock) {
if (sock != FAKE_SOCKET) return __real_socket_close_wrapper(sock);
assert(socket_open && !socket_registered);
socket_open = 0;
return 0;
}
ssize_t __wrap_socket_sendto(socket_t sock, const void* data, size_t len, const struct sockaddr* addr, socklen_t addr_len) {
assert(sock == FAKE_SOCKET && socket_open && len == sizeof(request));
assert(addr && addr_len == sizeof(struct sockaddr_in));
sends++;
memcpy(request, data, len);
errno = EIO;
return fail_send ? -1 : (ssize_t)len;
}
ssize_t __wrap_socket_recvfrom(socket_t sock, void* data, size_t len, struct sockaddr* addr, socklen_t* addr_len) {
assert(sock == FAKE_SOCKET && socket_open && len >= sizeof(reply));
(void)addr; (void)addr_len;
memcpy(data, reply, sizeof(reply));
return reply_length;
}
void* __wrap_uasync_add_socket_t(struct UASYNC* ua, socket_t sock, socket_t_callback_t r,
socket_t_callback_t w, socket_t_callback_t e, const char* name, void* arg) {
assert(ua && sock == FAKE_SOCKET && socket_open && !socket_registered && r && !w && !e);
assert(!strcmp(name, "ntp_query"));
if (fail_register) return NULL;
socket_registered = 1;
read_callback = r; read_arg = arg;
return &socket_registered;
}
err_t __wrap_uasync_remove_socket_t(struct UASYNC* ua, socket_t sock) {
assert(ua && sock == FAKE_SOCKET && socket_registered);
socket_registered = 0; read_callback = NULL; read_arg = NULL;
return ERR_OK;
}
struct adns_query* __wrap_adns_resolve(struct UASYNC* ua, const char* name, const struct adns_opts* opts,
adns_done_cb cb, void* arg) {
assert(ua && name && cb && !dns_callback);
(void)opts;
dns_calls++;
if (fail_dns) return NULL;
dns_callback = cb; dns_arg = arg;
return (struct adns_query*)&dns_callback;
}
void __wrap_adns_cancel(struct adns_query* query) {
assert(query == (struct adns_query*)&dns_callback && dns_callback);
dns_callback = NULL; dns_arg = NULL; dns_cancels++;
}
static void finish_dns(int status) {
assert(dns_callback);
adns_done_cb cb = dns_callback;
void* arg = dns_arg;
dns_callback = NULL; dns_arg = NULL;
struct adns_result result = {.status = status, .count = status == ADNS_OK ? 1 : 0};
result.addrs[0].sin_family = AF_INET;
result.addrs[0].sin_addr.s_addr = htonl(INADDR_LOOPBACK);
cb(&result, arg);
}
void __wrap_topo_node_update_my_addresses(struct UTUN_INSTANCE* inst) { assert(inst); address_updates++; }
int __wrap_etcp_send(struct ETCP_CONN* conn, struct ll_entry* e) {
assert(conn && e && e->len == 10 && e->dgram[0] == ETCP_ID_NTP_TIME);
peer_sends++;
queue_dgram_free(e); queue_entry_free(e);
return 0;
}
struct fixture {
struct UASYNC* ua;
struct UTUN_INSTANCE inst;
struct utun_config config;
struct CFG_NTP_SERVER servers[2];
struct ETCP_CONN conns[3];
};
static void setup(struct fixture* f) {
memset(f, 0, sizeof(*f));
memset(timers, 0, sizeof(timers));
wall_us = WALL_START_US;
creates = sends = peer_sends = address_updates = dns_calls = dns_cancels = 0;
fail_create = fail_nonblock = fail_send = fail_register = fail_dns = 0;
fail_alloc_at = alloc_attempts = 0; fail_timer = NULL;
f->ua = uasync_create();
assert(f->ua);
f->inst.ua = f->ua;
f->inst.config = &f->config;
f->inst.connections = queue_new(f->ua, 0, 0, 0, "ntp_test_connections");
assert(f->inst.connections);
f->config.global.ntp_enabled = 1;
f->config.global.ntp_resync_interval = 3600;
f->config.global.ntp_server_count = 2;
f->config.global.ntp_servers = &f->servers[0];
strcpy(f->servers[0].name, "a.invalid");
strcpy(f->servers[1].name, "b.invalid");
f->servers[0].next = &f->servers[1];
for (int i = 0; i < 3; i++) {
f->conns[i].instance = &f->inst;
f->conns[i].peer_node_id = i + 1;
}
}
static void cleanup(struct fixture* f) {
fail_alloc_at = 0;
ntp_time_destroy(&f->inst);
ntp_node_time_destroy(&f->inst);
assert(!socket_open && !socket_registered && !dns_callback && !read_callback);
for (int i = 0; i < 4; i++) assert(!timers[i].active);
assert(!f->inst.new_conn_cbks && !f->inst.api_bindings.callbacks[ETCP_ID_NTP_TIME]);
for (int i = 0; i < 3; i++) assert(!f->conns[i].cbks);
struct ll_entry* e;
while ((e = queue_get_data(f->inst.connections))) queue_entry_free(e);
queue_free(f->inst.connections);
uasync_destroy(f->ua, 0);
}
static void start(struct fixture* f, int direct) {
assert(!ntp_time_init(&f->inst));
if (direct) ntp_time_set_test_addr(&f->inst.ntp, "127.0.0.1", 11123);
assert(!f->inst.ntp.sync && f->inst.ntp.timer);
fire_timer(f->inst.ntp.timer);
}
static uint64_t ntp_stamp(int64_t us) {
return ((uint64_t)(us / 1000000 + 2208988800LL) << 32) | ((uint64_t)(us % 1000000) << 32) / 1000000;
}
static uint64_t request_stamp(void) {
uint64_t value;
memcpy(&value, request + 40, sizeof(value));
return be64toh(value);
}
static void deliver_reply(uint64_t origin, int64_t server_us) {
memset(reply, 0, sizeof(reply));
reply[0] = (4 << 3) | 4; reply[1] = 1;
uint64_t value = htobe64(origin);
memcpy(reply + 24, &value, sizeof(value));
value = htobe64(ntp_stamp(server_us));
memcpy(reply + 32, &value, sizeof(value));
memcpy(reply + 40, &value, sizeof(value));
reply_length = sizeof(reply);
assert(read_callback);
read_callback(FAKE_SOCKET, read_arg);
}
static void check_time(struct fixture* f, int64_t expected_us) {
assert(ntp_time_is_synced(&f->inst));
int64_t error = ntp_time_get_us(&f->inst) - expected_us;
assert(error >= -2 && error <= 2);
assert(ntp_time_get_seconds(&f->inst) == expected_us / 1000000);
}
static void test_offsets(struct fixture* f) {
for (int sign = -1; sign <= 1; sign++) {
start(f, 1);
deliver_reply(request_stamp(), wall_us + sign * 5000000LL);
assert(f->inst.ntp.offset_us == -sign * 5000000LL);
check_time(f, wall_us + sign * 5000000LL);
assert(f->inst.ntp.reachable && !f->inst.ntp.sync && f->inst.ntp.timer);
assert(((struct timer*)f->inst.ntp.timer)->delay_tb == 36000000);
ntp_time_destroy(&f->inst);
check_time(f, wall_us + sign * 5000000LL);
}
assert(!dns_calls);
}
static void test_retry_timestamp(struct fixture* f) {
start(f, 1);
uint64_t first = request_stamp();
fire_query_timer();
uint64_t second = request_stamp();
assert(first != second && sends == 2);
deliver_reply(first, wall_us + 5000000);
assert(!f->inst.ntp.synced && f->inst.ntp.sync);
deliver_reply(second, wall_us + 5000000);
check_time(f, wall_us + 5000000);
}
static void check_failed_cycle(struct fixture* f) {
assert(!f->inst.ntp.sync && !f->inst.ntp.reachable && f->inst.ntp.timer);
assert(!socket_open && !socket_registered && !dns_callback && address_updates == 1);
int active = 0;
for (int i = 0; i < 4; i++) active += timers[i].active;
assert(active == 1 && ((struct timer*)f->inst.ntp.timer)->delay_tb == 36000000);
}
static void test_async_dns_failure(struct fixture* f) {
start(f, 0);
finish_dns(ADNS_ERR_TIMEOUT);
assert(dns_calls == 2 && f->inst.ntp.sync);
finish_dns(ADNS_ERR_NODATA);
assert(dns_calls == 2);
check_failed_cycle(f);
// Следующий цикл действительно запускается и может восстановить reachable.
fire_timer(f->inst.ntp.timer);
finish_dns(ADNS_OK);
deliver_reply(request_stamp(), wall_us - 5000000);
check_time(f, wall_us - 5000000);
assert(f->inst.ntp.reachable && address_updates == 2 && f->inst.ntp.server_current == 1);
}
static void test_immediate_dns_failure(struct fixture* f) {
fail_dns = 1;
start(f, 0);
assert(dns_calls == 2);
check_failed_cycle(f);
}
static void test_udp_exhaustion(struct fixture* f) {
start(f, 0);
for (int server = 0; server < 2; server++) {
finish_dns(ADNS_OK);
for (int i = 0; i < 3; i++) fire_query_timer();
}
assert(dns_calls == 2 && creates == 2 && sends == 6);
check_failed_cycle(f);
}
static void test_immediate_udp_failures(struct fixture* f) {
strcpy(f->servers[0].name, "127.0.0.1");
strcpy(f->servers[1].name, "127.0.0.2");
int* faults[] = {&fail_create, &fail_nonblock, &fail_send, &fail_register};
for (size_t i = 0; i < sizeof(faults) / sizeof(faults[0]); i++) {
creates = sends = address_updates = 0;
*faults[i] = 1;
start(f, 0);
assert(creates == 2 && !dns_calls);
check_failed_cycle(f);
ntp_time_destroy(&f->inst);
*faults[i] = 0;
}
}
static void test_timer_failures(struct fixture* f) {
fail_timer = "ntp_sync";
assert(ntp_time_init(&f->inst) == -1);
assert(!f->inst.ntp.servers && !f->inst.ntp.timer && !f->inst.ntp.enabled);
assert(!ntp_time_init(&f->inst));
ntp_time_set_test_addr(&f->inst.ntp, "127.0.0.1", 11123);
fail_timer = "ntp_q";
fire_timer(f->inst.ntp.timer);
check_failed_cycle(f);
ntp_time_destroy(&f->inst);
start(f, 1);
fail_timer = "ntp_sync";
deliver_reply(request_stamp(), wall_us + 5000000);
check_time(f, wall_us + 5000000);
assert(!f->inst.ntp.sync && !f->inst.ntp.timer && !socket_open);
}
static void test_cancel(struct fixture* f) {
start(f, 0);
assert(dns_callback);
ntp_time_destroy(&f->inst);
assert(!dns_callback && dns_cancels == 1 && !f->inst.ntp.sync);
start(f, 1);
assert(socket_registered);
ntp_time_destroy(&f->inst);
assert(!read_callback && !socket_open && !f->inst.ntp.sync);
}
static void test_init_failures(struct fixture* f) {
size_t baseline = u_get_allocated_count();
for (int i = 1; i <= 3; i++) {
alloc_attempts = 0; fail_alloc_at = i;
assert(ntp_time_init(&f->inst) == -1);
fail_alloc_at = 0;
assert(!f->inst.ntp.servers && !f->inst.ntp.timer && u_get_allocated_count() == baseline);
}
f->config.global.ntp_resync_interval = INT_MAX;
assert(ntp_time_init(&f->inst) == -1);
f->config.global.ntp_resync_interval = 3600;
assert(!ntp_time_init(&f->inst));
void* timer = f->inst.ntp.timer;
assert(ntp_time_init(&f->inst) == -1 && timer == f->inst.ntp.timer);
alloc_attempts = 0; fail_alloc_at = 1;
fire_timer(timer);
fail_alloc_at = 0;
assert(!f->inst.ntp.sync && f->inst.ntp.timer);
}
static void register_connection(struct fixture* f, int index) {
struct ll_entry* e = queue_entry_new(sizeof(struct conn_queue_entry));
assert(e);
((struct conn_queue_entry*)e->data)->conn = &f->conns[index];
assert(!queue_data_put(f->inst.connections, e));
for (struct etcp_inst_cbk_entry* cb = f->inst.new_conn_cbks; cb; cb = cb->next) cb->fn(&f->conns[index], cb->arg);
}
static void receive_peer(struct fixture* f, struct ETCP_CONN* conn, int synced, int64_t time_us) {
struct ll_entry* e = queue_entry_new(0);
assert(e);
e->dgram = u_malloc(10);
assert(e->dgram);
e->len = 10; e->dgram[0] = ETCP_ID_NTP_TIME; e->dgram[1] = synced;
memcpy(e->dgram + 2, &time_us, sizeof(time_us));
assert(f->inst.api_bindings.callbacks[ETCP_ID_NTP_TIME]);
f->inst.api_bindings.callbacks[ETCP_ID_NTP_TIME](conn, e);
}
static void test_peer_cache(struct fixture* f) {
assert(!ntp_node_time_init(&f->inst));
struct ETCP_CONN* conn = &f->conns[0];
size_t baseline = u_get_allocated_count();
for (int i = 0; i < NTP_NODE_MAX_PEERS; i++) {
conn->peer_node_id = i + 1;
receive_peer(f, conn, 0, wall_us);
f->inst.ntp_node.peers[i].last_seen_tb = i + 100;
}
conn->peer_node_id = 1;
receive_peer(f, conn, 0, wall_us); // обновление сохраняет запись 1; вытеснить надо запись 2
conn->peer_node_id = 100;
struct ETCP_LINK link = {.rtt_last = 200}; // 20 мс, половина — 10000 мкс
conn->links = &link;
receive_peer(f, conn, 1, wall_us + 4990000);
conn->links = NULL;
check_time(f, wall_us + 5000000);
assert(!f->inst.ntp.enabled && f->inst.ntp_node.peer_count == NTP_NODE_MAX_PEERS);
assert(f->inst.ntp_node.peers[0].node_id == 1 && f->inst.ntp_node.peers[1].node_id == 100);
// Ещё один новый пир получает ответ и не меняет уже принятую поправку.
conn->peer_node_id = 101;
receive_peer(f, conn, 0, wall_us - 5000000);
check_time(f, wall_us + 5000000);
assert(peer_sends == 1 && u_get_allocated_count() == baseline);
ntp_node_time_destroy(&f->inst);
assert(!f->inst.ntp_node.peer_count);
check_time(f, wall_us + 5000000);
}
static int foreign_calls;
static void foreign_callback(struct ETCP_CONN* conn, int event, void* arg) {
(void)conn; (void)event; (void)arg;
foreign_calls++;
}
static void test_node_lifecycle(struct fixture* f) {
register_connection(f, 0); // pending до init
register_connection(f, 1); // initialized до init
f->conns[1].initialized = f->conns[1].links_up = 1;
etcp_conn_add_cbk(&f->conns[0], foreign_callback, f, ETCP_CBK_EVENT_INIT);
struct etcp_cbk_entry* foreign = f->conns[0].cbks;
assert(foreign);
for (int cycle = 0; cycle < 2; cycle++) {
int before = peer_sends;
assert(!ntp_node_time_init(&f->inst));
assert(peer_sends == before + 1);
assert(!ntp_node_time_init(&f->inst) && peer_sends == before + 1);
if (!cycle) register_connection(f, 2); // новое pending после init
etcp_cbk_fire(&f->conns[0], ETCP_CBK_EVENT_INIT);
etcp_cbk_fire(&f->conns[2], ETCP_CBK_EVENT_INIT);
assert(peer_sends == before + 3);
ntp_node_time_destroy(&f->inst);
ntp_node_time_destroy(&f->inst);
assert(f->conns[0].cbks == foreign && !foreign->next);
assert(!f->conns[1].cbks && !f->conns[2].cbks);
etcp_cbk_fire(&f->conns[0], ETCP_CBK_EVENT_INIT);
etcp_cbk_fire(&f->conns[2], ETCP_CBK_EVENT_INIT);
assert(peer_sends == before + 3);
}
assert(foreign_calls == 4);
etcp_conn_remove_cbk(&f->conns[0], foreign_callback, f);
// Отказ регистрации на каждом шаге должен снять уже установленные подписки.
for (int i = 1; i <= 4; i++) {
alloc_attempts = 0; fail_alloc_at = i;
assert(ntp_node_time_init(&f->inst) == -1);
fail_alloc_at = 0;
assert(!f->inst.ntp_node.active && !f->inst.new_conn_cbks);
assert(!f->inst.api_bindings.callbacks[ETCP_ID_NTP_TIME]);
for (int j = 0; j < 3; j++) assert(!f->conns[j].cbks);
}
}
int main(void) {
debug_config_init();
debug_set_level(DEBUG_LEVEL_WARN);
debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_INFO);
void (*tests[])(struct fixture*) = {test_offsets, test_retry_timestamp, test_async_dns_failure,
test_immediate_dns_failure, test_udp_exhaustion, test_immediate_udp_failures, test_timer_failures,
test_cancel, test_init_failures, test_peer_cache, test_node_lifecycle};
for (size_t i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
size_t baseline = u_get_allocated_count();
struct fixture f;
setup(&f);
tests[i](&f);
cleanup(&f);
assert(u_get_allocated_count() == baseline);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP regression %zu passed, no live allocations", i + 1);
}
return 0;
}