/** * @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 #include #include #include #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; }