You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

258 lines
9.1 KiB

/**
* @file test_ntp.c
* @brief NTP client test with local fake NTP server
*
* Starts a local NTP server in a separate thread that returns a known
* time offset. Verifies the NTP client syncs correctly: synced flag,
* offset computation, and ntp_time_get_us()/ntp_time_is_synced().
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include "../lib/platform_compat.h"
#include "../lib/socket_compat.h"
#include "../lib/debug_config.h"
#include "../lib/u_async.h"
#include "../src/utun_instance.h"
#include "../src/ntp_time.h"
#include "../src/config_parser.h"
#define TEST_KNOWN_OFFSET_SEC 5
#define TEST_KNOWN_OFFSET_US (TEST_KNOWN_OFFSET_SEC * 1000000LL)
#define TEST_TIMEOUT_SEC 10
#define OFFSET_TOLERANCE_US 500000 // 500ms — generous for scheduling jitter on localhost
#define SYNC_WAIT_POLL_MS 10
#define NTP_DELTA 2208988800ULL
#pragma pack(push, 1)
struct ntp_packet {
uint8_t li_vn_mode;
uint8_t stratum;
uint8_t poll;
uint8_t precision;
uint32_t root_delay;
uint32_t root_dispersion;
uint32_t ref_id;
uint64_t ref_ts;
uint64_t orig_ts;
uint64_t recv_ts;
uint64_t xmit_ts;
};
#pragma pack(pop)
_Static_assert(sizeof(struct ntp_packet) == 48, "bad ntp_packet size");
static volatile int g_server_running = 1;
static volatile uint16_t g_server_port = 0;
static uint64_t timeval_to_ntp(struct timeval *tv) {
uint64_t sec = (uint64_t)(tv->tv_sec + NTP_DELTA);
uint64_t frac = ((uint64_t)tv->tv_usec << 32) / 1000000ULL;
return (sec << 32) | frac;
}
static void* test_server_thread(void* arg) {
(void)arg;
socket_t sock = socket_create_udp(AF_INET);
if (sock == SOCKET_INVALID) { g_server_running = 0; return NULL; }
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = 0;
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { socket_close_wrapper(sock); g_server_running = 0; return NULL; }
socklen_t addr_len = sizeof(addr);
getsockname(sock, (struct sockaddr*)&addr, &addr_len);
g_server_port = ntohs(addr.sin_port);
struct timeval tv = {1, 0};
while (g_server_running) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(sock, &fds);
tv.tv_sec = 1; tv.tv_usec = 0;
int r = select((int)(sock + 1), &fds, NULL, NULL, &tv);
if (r <= 0) continue;
struct ntp_packet request;
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
ssize_t n = recvfrom(sock, (char*)&request, sizeof(request), 0, (struct sockaddr*)&client, &client_len);
if (n < (ssize_t)sizeof(request)) continue;
uint8_t mode = request.li_vn_mode & 0x07;
if (mode != 3) continue; // only respond to client-mode
struct timeval now;
utun_gettimeofday(&now, NULL);
now.tv_sec += TEST_KNOWN_OFFSET_SEC;
uint64_t fake_ntp = timeval_to_ntp(&now);
struct ntp_packet reply;
memset(&reply, 0, sizeof(reply));
reply.li_vn_mode = (0 << 6) | (4 << 3) | 4; // stratum 0=unspec, v4, server
reply.stratum = 1;
reply.ref_id = htonl(0x7F000001); // 127.0.0.1
reply.orig_ts = request.xmit_ts; // echo client's transmit
reply.recv_ts = htobe64(fake_ntp);
reply.xmit_ts = htobe64(fake_ntp);
sendto(sock, (const char*)&reply, sizeof(reply), 0, (struct sockaddr*)&client, sizeof(client));
}
socket_close_wrapper(sock);
return NULL;
}
static void platform_sleep_ms(int ms) {
#ifdef _WIN32
Sleep(ms);
#else
usleep(ms * 1000);
#endif
}
static int wait_for_port(void) {
for (int i = 0; i < 200; i++) {
if (g_server_port != 0) return 0;
platform_sleep_ms(5);
}
return -1;
}
int main(void) {
int result = 1;
debug_config_init();
debug_set_level(DEBUG_LEVEL_WARN);
debug_set_categories(DEBUG_CATEGORY_GENERAL);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "=== NTP Test ===");
/* ── 1. Start fake NTP server thread ── */
pthread_t server_thread;
if (pthread_create(&server_thread, NULL, test_server_thread, NULL) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Failed to create server thread");
return 1;
}
if (wait_for_port() != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Server thread did not bind");
g_server_running = 0;
pthread_join(server_thread, NULL);
return 1;
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Fake NTP server on 127.0.0.1:%u, offset=+%ds", g_server_port, TEST_KNOWN_OFFSET_SEC);
/* ── 2. Create uTun instance with minimal config ── */
struct UASYNC* ua = uasync_create();
if (!ua) { g_server_running = 0; pthread_join(server_thread, NULL); return 1; }
utun_instance_set_tun_init_enabled(0);
/* Parse config from buffer and inject keys (avoid config_ensure_keys_and_node_id rewriting) */
const char *config_text =
"[global]\n"
"my_node_name=ntp_test\n"
"my_private_key=e8d4d14943ca3732d7802d4010ea4f6f8bdb71e2304f78d68f4f3dc31e760c79\n"
"my_public_key=8bac8ff3b3344cd3c1168b4e55bb4673f4063b5d42afb05fc70658f5da27907c\n"
"my_node_id=721256a8d204e355\n"
"\n"
"[ntp]\n"
"enabled=yes\n"
"server=placeholder\n"
"interval=3600\n";
struct utun_config* cfg = parse_config_from_buf(config_text, strlen(config_text), "inline");
if (!cfg) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Failed to parse config from buffer");
uasync_destroy(ua, 0);
g_server_running = 0;
pthread_join(server_thread, NULL);
return 1;
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Parsed config: ntp_enabled=%d server_count=%d",
cfg->global.ntp_enabled, cfg->global.ntp_server_count);
struct UTUN_INSTANCE* inst = utun_instance_create_from_config(ua, cfg);
if (!inst) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Failed to create instance from config");
free_config(cfg);
uasync_destroy(ua, 0);
g_server_running = 0;
pthread_join(server_thread, NULL);
return 1;
}
/* utun_instance_create only calls instance_init_common (basic inits).
utun_instance_init is needed for NTP, connections, etc. */
utun_instance_init(inst);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP state after init: enabled=%d server_count=%d interval=%d servers[0]=%s config_enabled=%d config_count=%d",
inst->ntp.enabled, inst->ntp.server_count, inst->ntp.resync_interval_sec,
inst->ntp.server_count > 0 ? inst->ntp.servers[0] : "NONE",
inst->config ? inst->config->global.ntp_enabled : -1,
inst->config ? inst->config->global.ntp_server_count : -1);
/* ── 3. Set test address BEFORE first uasync poll (timer fires at delay 0) ── */
ntp_time_set_test_addr(&inst->ntp, "127.0.0.1", g_server_port);
/* ── 4. Poll until synced or timeout ── */
uint64_t start_tb = get_time_tb();
while (get_time_tb() - start_tb < (uint64_t)(TEST_TIMEOUT_SEC * 10000)) {
uasync_poll(ua, SYNC_WAIT_POLL_MS);
if (inst->ntp.synced) break;
}
/* ── 5. Verify ── */
if (!inst->ntp.synced) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: NTP did not sync within %ds", TEST_TIMEOUT_SEC);
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP synced, offset=%lldus", (long long)inst->ntp.offset_us);
// Check offset is close to expected (formula gives server - client ≈ KNOWN_OFFSET)
{
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: offset %lldus too far from expected %lldus (diff=%lldus)",
(long long)inst->ntp.offset_us, (long long)TEST_KNOWN_OFFSET_US, (long long)diff);
goto cleanup;
}
}
// Check ntp_time_is_synced
if (!ntp_time_is_synced(inst)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: ntp_time_is_synced returned 0 after sync");
goto cleanup;
}
// Check ntp_time_get_us internal consistency
{
struct timeval tv;
utun_gettimeofday(&tv, NULL);
int64_t raw_us = (int64_t)tv.tv_sec * 1000000LL + tv.tv_usec;
int64_t corrected_us = ntp_time_get_us(inst);
int64_t expected_us = raw_us - inst->ntp.offset_us;
int64_t diff = corrected_us - expected_us;
if (diff < 0) diff = -diff;
if (diff > 10000) { // 10ms — time passes between gettimeofday calls
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: ntp_time_get_us(%lld) != gettimeofday(%lld) - offset(%lld), diff=%lldus",
(long long)corrected_us, (long long)raw_us, (long long)inst->ntp.offset_us, (long long)diff);
goto cleanup;
}
}
result = 0;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP Test PASSED");
cleanup:
utun_instance_destroy(inst);
uasync_destroy(ua, 0);
g_server_running = 0;
pthread_join(server_thread, NULL);
return result;
}