Browse Source

dns: async_dns модуль (libdns) + неблокирующий NTP sync

- Вендор libdns (MIT) как lib/dns.{c,h}; правка dns_quietinit для GCC 5+
- Новый lib/async_dns.{c,h}: неблокирующий A-резолвер поверх uasync
- src/ntp_time.c: убраны блокирующие getaddrinfo+select; async DNS + неблокирующая машина состояний UDP-запроса
- test_async_dns.c (юнит) + test_ntp.c сценарий hostname→DNS
- сборка: -DDNS_RANDOM=RAND_bytes, -lcrypto в COMMON_LIBS
topo_upd
Evgeny 2 months ago
parent
commit
1f307046f9
  1. 7
      lib/Makefile.am
  2. 386
      lib/async_dns.c
  3. 67
      lib/async_dns.h
  4. 9105
      lib/dns.c
  5. 1199
      lib/dns.h
  6. 389
      src/ntp_time.c
  7. 4
      src/ntp_time.h
  8. 11
      tests/Makefile.am
  9. 291
      tests/test_async_dns.c
  10. 265
      tests/test_ntp.c

7
lib/Makefile.am

@ -36,12 +36,17 @@ libuasync_a_SOURCES = \
audio_compressor.c \
audio_compressor.h \
strbuf.c \
strbuf.h
strbuf.h \
dns.c \
dns.h \
async_dns.c \
async_dns.h
libuasync_a_CFLAGS = \
-D_ISOC99_SOURCE \
-DDEBUG_OUTPUT_STDERR \
-DSQLITE_THREADSAFE=1 \
-DDNS_RANDOM=RAND_bytes \
-g \
-I$(top_srcdir)/src \
-I$(top_srcdir)/lib \

386
lib/async_dns.c

@ -0,0 +1,386 @@
/*
* async_dns.c — асинхронный (неблокирующий) DNS-резолвер поверх uasync.
*
* Адаптер над libdns (lib/dns.c). Схема работы:
* adns_resolve() → dns_res_submit(A-запрос) → adns_drive()
* adns_drive() → dns_res_check():
* 0 → готово, dns_res_fetch → разбор A-записей → коллбэк
* EAGAIN → зарегистрировать fd (dns_res_pollfd/events) + таймер
* (dns_res_timeout) в uasync и ждать события
* иначе → ошибка → коллбэк с ADNS_ERR_RESPONSE
*
* Всё выполняется в потоке event loop, блокирующих вызовов нет.
*/
#include "async_dns.h"
#include "dns.h"
#include "debug_config.h"
#include "mem.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#ifdef _WIN32
#include <iphlpapi.h>
#endif
#define ADNS_DEBUG_CAT DEBUG_CATEGORY_SOCKET
struct adns_query {
struct UASYNC* ua;
adns_done_cb cb;
void* arg;
struct dns_resolver* res;
void* socket_id; /* handle uasync (NULL когда не зарегистрирован) */
int cur_fd; /* текущий зарегистрированный fd (-1 когда нет) */
void* timer; /* handle таймаута (NULL когда нет) */
int finished;
char* name;
};
static void adns_drive(struct adns_query* q);
static void adns_finish(struct adns_query* q, int status, struct sockaddr_in* addrs, int count);
static void adns_read_cb(socket_t sock, void* arg);
static void adns_write_cb(socket_t sock, void* arg);
static void adns_timer_cb(void* arg);
/* ─── system DNS servers discovery ─── */
int adns_parse_resolv_conf(const char* text, struct sockaddr_in* out, int max) {
int n = 0;
const char* p = text;
if (!text || !out || max <= 0) return 0;
while (*p && n < max) {
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++;
if (!*p) break;
if (strncmp(p, "nameserver", 10) == 0 && (p[10] == ' ' || p[10] == '\t')) {
p += 10;
while (*p == ' ' || *p == '\t') p++;
char tok[64];
int i = 0;
while (*p && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && *p != '#' && i < 63)
tok[i++] = *p++;
tok[i] = '\0';
memset(&out[n], 0, sizeof(out[n]));
if (tok[0] && inet_pton(AF_INET, tok, &out[n].sin_addr) == 1) {
out[n].sin_family = AF_INET;
out[n].sin_port = htons(53);
n++;
}
}
while (*p && *p != '\n') p++;
if (*p) p++;
}
return n;
}
#ifdef _WIN32
int adns_system_servers(struct sockaddr_in* out, int max) {
if (!out || max <= 0) return 0;
ULONG size = 0;
if (GetNetworkParams(NULL, &size) != ERROR_BUFFER_OVERFLOW) return 0;
PFIXED_INFO fi = (PFIXED_INFO)u_malloc(size);
if (!fi) return 0;
if (GetNetworkParams(fi, &size) != NO_ERROR) { u_free(fi); return 0; }
int n = 0;
IP_ADDR_STRING* p = &fi->DnsServerList;
while (p && n < max) {
const char* s = p->IpAddress;
while (*s && n < max) {
while (*s == ' ' || *s == ',') s++;
char tok[64];
int i = 0;
while (*s && *s != ' ' && *s != ',' && i < 63) tok[i++] = *s++;
tok[i] = '\0';
memset(&out[n], 0, sizeof(out[n]));
if (tok[0] && inet_pton(AF_INET, tok, &out[n].sin_addr) == 1) {
out[n].sin_family = AF_INET;
out[n].sin_port = htons(53);
n++;
}
}
p = p->Next;
}
u_free(fi);
return n;
}
#else
int adns_system_servers(struct sockaddr_in* out, int max) {
if (!out || max <= 0) return 0;
FILE* f = fopen("/etc/resolv.conf", "r");
if (!f) return 0;
char buf[4096];
size_t rd = fread(buf, 1, sizeof(buf) - 1, f);
fclose(f);
buf[rd] = '\0';
return adns_parse_resolv_conf(buf, out, max);
}
#endif
/* ─── resolver setup ─── */
static struct dns_resolver* adns_open_resolver(const struct sockaddr_in* servers, int count,
int timeout_ms, int attempts) {
int error = 0;
struct dns_resolv_conf* rc = dns_resconf_open(&error);
if (!rc) {
DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: dns_resconf_open failed: %s", dns_strerror(error));
return NULL;
}
for (int i = 0; i < count && i < 3; i++) {
struct sockaddr_storage* ss = &rc->nameserver[i];
struct sockaddr_in* sin = (struct sockaddr_in*)ss;
memset(ss, 0, sizeof(*ss));
sin->sin_family = AF_INET;
sin->sin_addr = servers[i].sin_addr;
sin->sin_port = servers[i].sin_port ? servers[i].sin_port : htons(53);
}
if (timeout_ms > 0) {
unsigned sec = (unsigned)((timeout_ms + 999) / 1000);
rc->options.timeout = sec ? sec : 1;
}
if (attempts > 0) rc->options.attempts = (unsigned)attempts;
struct dns_hosts* hosts = dns_hosts_open(&error);
if (!hosts) {
DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: dns_hosts_open failed: %s", dns_strerror(error));
dns_resconf_close(rc);
return NULL;
}
struct dns_hints* hints = dns_hints_local(rc, &error);
if (!hints) {
DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: dns_hints_local failed: %s", dns_strerror(error));
dns_hosts_close(hosts);
dns_resconf_close(rc);
return NULL;
}
struct dns_resolver* res = dns_res_open(rc, hosts, hints, NULL, NULL, &error);
dns_resconf_close(rc);
dns_hosts_close(hosts);
dns_hints_close(hints);
if (!res)
DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: dns_res_open failed: %s", dns_strerror(error));
return res;
}
/* ─── driving state machine ─── */
static void adns_teardown(struct adns_query* q) {
if (q->timer) { uasync_cancel_timeout(q->ua, q->timer); q->timer = NULL; }
if (q->socket_id) {
uasync_remove_socket_t(q->ua, (socket_t)q->cur_fd);
q->socket_id = NULL;
}
q->cur_fd = -1;
if (q->res) { dns_res_close(q->res); q->res = NULL; }
}
static void adns_rearm(struct adns_query* q) {
int fd = dns_res_pollfd(q->res);
int events = dns_res_events(q->res);
if (fd != q->cur_fd) {
if (q->socket_id) {
uasync_remove_socket_t(q->ua, (socket_t)q->cur_fd);
q->socket_id = NULL;
}
q->cur_fd = -1;
if (fd >= 0) {
q->socket_id = uasync_add_socket_t(q->ua, (socket_t)fd, adns_read_cb, adns_write_cb, NULL, q);
if (q->socket_id) q->cur_fd = fd;
else DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: uasync_add_socket_t(%d) failed", fd);
}
}
if (q->socket_id) {
uasync_set_socket_read(q->ua, q->socket_id, (events & DNS_POLLIN) ? 1 : 0);
uasync_set_socket_write(q->ua, q->socket_id, (events & DNS_POLLOUT) ? 1 : 0);
}
time_t t = dns_res_timeout(q->res);
if (t < 1) t = 1;
if (q->timer) { uasync_cancel_timeout(q->ua, q->timer); q->timer = NULL; }
q->timer = uasync_set_timeout(q->ua, (int)(t * 10000), q, adns_timer_cb, "adns");
if (!q->timer)
DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: uasync_set_timeout failed");
}
static void adns_drive(struct adns_query* q) {
if (q->finished) return;
int error = dns_res_check(q->res);
if (error == 0) {
int ferr = 0;
struct dns_packet* answer = dns_res_fetch(q->res, &ferr);
if (!answer) {
DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: '%s' dns_res_fetch failed: %s", q->name, dns_strerror(ferr));
adns_finish(q, ADNS_ERR_RESPONSE, NULL, 0);
return;
}
unsigned rcode = dns_p_rcode(answer);
if (rcode == DNS_RC_NOERROR) {
struct sockaddr_in addrs[ADNS_MAX_ADDRS];
int n = 0;
struct dns_rr rr;
struct dns_a a;
dns_rr_foreach(&rr, answer, .section = DNS_S_AN, .type = DNS_T_A) {
if (n >= ADNS_MAX_ADDRS) break;
dns_a_parse(&a, &rr, answer);
memset(&addrs[n], 0, sizeof(addrs[n]));
addrs[n].sin_family = AF_INET;
addrs[n].sin_addr = a.addr;
n++;
}
free(answer);
if (n > 0) {
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addrs[0].sin_addr, ip, sizeof(ip));
DEBUG_INFO(ADNS_DEBUG_CAT, "adns: '%s' resolved to %d A record(s), first=%s", q->name, n, ip);
adns_finish(q, ADNS_OK, addrs, n);
} else {
DEBUG_WARN(ADNS_DEBUG_CAT, "adns: '%s' no A records (NOERROR, empty answer)", q->name);
adns_finish(q, ADNS_ERR_NODATA, NULL, 0);
}
} else {
const char* rc = dns_strrcode((enum dns_rcode)rcode);
if (rcode == DNS_RC_NXDOMAIN) {
free(answer);
DEBUG_WARN(ADNS_DEBUG_CAT, "adns: '%s' NXDOMAIN", q->name);
adns_finish(q, ADNS_ERR_NODATA, NULL, 0);
} else if (rcode == DNS_RC_SERVFAIL) {
/* libdns синтезирует SERVFAIL и при таймауте (ответа не получено) */
const struct dns_stat* st = dns_res_stat(q->res);
int no_resp = st && st->udp.rcvd.count == 0;
free(answer);
if (no_resp) {
DEBUG_WARN(ADNS_DEBUG_CAT, "adns: '%s' timed out (no response from nameservers)", q->name);
adns_finish(q, ADNS_ERR_TIMEOUT, NULL, 0);
} else {
DEBUG_WARN(ADNS_DEBUG_CAT, "adns: '%s' SERVFAIL", q->name);
adns_finish(q, ADNS_ERR_RESPONSE, NULL, 0);
}
} else {
free(answer);
DEBUG_WARN(ADNS_DEBUG_CAT, "adns: '%s' rcode=%s — response error", q->name, rc);
adns_finish(q, ADNS_ERR_RESPONSE, NULL, 0);
}
}
return;
}
if (error == EAGAIN) {
adns_rearm(q);
return;
}
DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: '%s' dns_res_check error=%d (%s)", q->name, error, dns_strerror(error));
adns_finish(q, ADNS_ERR_RESPONSE, NULL, 0);
}
static void adns_read_cb(socket_t sock, void* arg) {
(void)sock;
adns_drive((struct adns_query*)arg);
}
static void adns_write_cb(socket_t sock, void* arg) {
(void)sock;
adns_drive((struct adns_query*)arg);
}
static void adns_timer_cb(void* arg) {
struct adns_query* q = (struct adns_query*)arg;
q->timer = NULL;
adns_drive(q);
}
static void adns_finish(struct adns_query* q, int status, struct sockaddr_in* addrs, int count) {
if (q->finished) return;
q->finished = 1;
adns_teardown(q);
struct adns_result res;
res.status = status;
res.count = count > 0 ? count : 0;
if (res.count > 0 && addrs) memcpy(res.addrs, addrs, (size_t)res.count * sizeof(*addrs));
adns_done_cb cb = q->cb;
void* arg = q->arg;
char* name = q->name;
q->name = NULL;
u_free(q);
if (cb) cb(&res, arg);
u_free(name);
}
/* ─── public API ─── */
struct adns_query* adns_resolve(struct UASYNC* ua, const char* name,
const struct adns_opts* opts,
adns_done_cb cb, void* arg) {
if (!ua || !name || !name[0] || !cb) return NULL;
if (strlen(name) > 255) { DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: name too long"); return NULL; }
struct sockaddr_in servers[3];
int count;
if (opts && opts->server.sin_family != 0) {
servers[0] = opts->server;
count = 1;
} else {
count = adns_system_servers(servers, 3);
}
if (count <= 0) {
DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: no DNS servers available for '%s'", name);
return NULL;
}
int timeout_ms = (opts && opts->timeout_ms > 0) ? opts->timeout_ms : 2000;
int attempts = (opts && opts->max_attempts > 0) ? opts->max_attempts : 2;
struct adns_query* q = u_calloc(1, sizeof(*q));
if (!q) { DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: calloc failed"); return NULL; }
q->ua = ua;
q->cb = cb;
q->arg = arg;
q->cur_fd = -1;
q->name = u_strdup(name);
if (!q->name) { u_free(q); DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: strdup failed"); return NULL; }
q->res = adns_open_resolver(servers, count, timeout_ms, attempts);
if (!q->res) { u_free(q->name); u_free(q); return NULL; }
if (dns_res_submit(q->res, name, DNS_T_A, DNS_C_IN) != 0) {
DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: dns_res_submit failed for '%s'", name);
dns_res_close(q->res);
u_free(q->name);
u_free(q);
return NULL;
}
DEBUG_DEBUG(ADNS_DEBUG_CAT, "adns: resolving '%s' via %d server(s), timeout=%dms attempts=%d",
name, count, timeout_ms, attempts);
adns_drive(q);
return q;
}
void adns_cancel(struct adns_query* q) {
if (!q || q->finished) return;
q->finished = 1;
adns_teardown(q);
u_free(q->name);
u_free(q);
}

67
lib/async_dns.h

@ -0,0 +1,67 @@
/*
* async_dns.h — асинхронный (неблокирующий) DNS-резолвер поверх uasync.
*
* Тонкий адаптер над libdns (lib/dns.c, MIT). Выполняет A-запросы без
* блокировки event loop: резолвер libdns управляется через uasync-сокеты и
* таймеры (интерфейс pollfd/events/timeout + submit/check/fetch).
*
* Поддерживает только A-записи (IPv4) — этого достаточно для NTP и текущего
* стека. TCP-fallback при truncation и компрессия имён — внутри libdns.
*/
#ifndef ASYNC_DNS_H
#define ASYNC_DNS_H
#include "u_async.h"
#include "socket_compat.h"
#ifdef __cplusplus
extern "C" {
#endif
#define ADNS_OK 0
#define ADNS_ERR_NOMEM -1
#define ADNS_ERR_PARAM -2
#define ADNS_ERR_NOSERV -3 /* нет ни одного DNS-сервера */
#define ADNS_ERR_NODATA -4 /* NXDOMAIN или нет A-записей */
#define ADNS_ERR_TIMEOUT -5
#define ADNS_ERR_RESPONSE -6 /* malformed / SERVFAIL / прочая ошибка ответа */
#define ADNS_MAX_ADDRS 8
struct adns_result {
int status;
int count;
struct sockaddr_in addrs[ADNS_MAX_ADDRS]; /* AF_INET, sin_port = 0 */
};
typedef void (*adns_done_cb)(const struct adns_result* res, void* arg);
struct adns_opts {
struct sockaddr_in server; /* sin_family != 0 → использовать этот сервер (иначе системные) */
int timeout_ms; /* таймаут на сервер, по умолчанию 2000 */
int max_attempts; /* попыток на сервер, по умолчанию 2 */
};
struct adns_query; /* opaque */
/* Запускает асинхронный A-запрос. Коллбэк вызывается в потоке event loop.
Возвращает handle (NULL при немедленной ошибке: невалидные аргументы/нет памяти). */
struct adns_query* adns_resolve(struct UASYNC* ua, const char* name,
const struct adns_opts* opts,
adns_done_cb cb, void* arg);
/* Отмена ожидающего запроса: коллбэк НЕ вызывается. Только из потока loop. */
void adns_cancel(struct adns_query* q);
/* Системные DNS-серверы (IPv4). Возвращает количество; out[].sin_port = htons(53).
Локальное чтение (resolv.conf / GetNetworkParams), без сети. */
int adns_system_servers(struct sockaddr_in* out, int max);
/* Чистый парсер текста resolv.conf ("nameserver <ip>") — для юнит-тестов. */
int adns_parse_resolv_conf(const char* text, struct sockaddr_in* out, int max);
#ifdef __cplusplus
}
#endif
#endif /* ASYNC_DNS_H */

9105
lib/dns.c

File diff suppressed because it is too large Load Diff

1199
lib/dns.h

File diff suppressed because it is too large Load Diff

389
src/ntp_time.c

@ -5,19 +5,10 @@
#include "../lib/socket_compat.h"
#include "../lib/u_async.h"
#include "../lib/mem.h"
#include "../lib/async_dns.h"
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <sys/select.h>
#include <sys/time.h>
#include <netdb.h>
#include <arpa/inet.h>
#endif
#define NTP_DELTA 2208988800ULL // seconds from 1900 to 1970
#define NTP_PORT 123
#define NTP_TIMEOUT_SEC 2
@ -41,7 +32,32 @@ struct ntp_packet {
_Static_assert(sizeof(struct ntp_packet) == 48, "NTP packet must be 48 bytes");
/* ─── синхронизация: неблокирующая машина состояний на uasync ─── */
struct ntp_sync {
struct UTUN_INSTANCE* inst;
int server_idx; /* следующий сервер для попытки */
int query_idx; /* сервер, который сейчас опрашиваем (-1 для test_addr) */
int resolving_idx; /* сервер в процессе DNS-резолва */
struct adns_query* dns_q; /* активный DNS-запрос (NULL когда нет) */
socket_t sock; /* NTP UDP-сокет (SOCKET_INVALID когда закрыт) */
void* socket_id; /* handle uasync */
void* timer; /* handle таймаута запроса */
uint8_t req[48]; /* NTP-запрос (xmit_ts заполняется один раз на сервер) */
uint64_t t1_ntp; /* originate timestamp */
int tries; /* попыток на текущем сервере */
struct sockaddr_in qaddr;/* адрес текущего сервера */
};
static void ntp_time_sync_cb(void* arg);
static void ntp_try_next(struct ntp_sync* ctx);
static void ntp_start_query(struct ntp_sync* ctx, int idx, const struct sockaddr_in* addr);
static void ntp_send_request(struct ntp_sync* ctx);
static void ntp_query_fail(struct ntp_sync* ctx);
static void ntp_finish_cycle(struct ntp_sync* ctx, int synced);
static void ntp_resolve_done_cb(const struct adns_result* res, void* arg);
static void ntp_query_read_cb(socket_t sock, void* arg);
static void ntp_query_timeout_cb(void* arg);
static uint64_t timeval_to_ntp(struct timeval *tv) {
uint64_t sec = (uint64_t)(tv->tv_sec + NTP_DELTA);
@ -55,13 +71,9 @@ static int64_t ntp64_to_us(uint64_t ntp) {
return sec * 1000000LL + frac;
}
static int ntp_do_query(const struct sockaddr_in* sin, const char* server_name, int64_t* offset_us_out, int* stratum_out) {
socket_t sock = socket_create_udp(AF_INET);
if (sock == SOCKET_INVALID) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: socket() failed: %s", socket_strerror(socket_get_error()));
return -1;
}
/* ─── чистые функции сборки/разбора пакета ─── */
static void ntp_build_request(uint8_t* req, uint64_t* t1_ntp_out) {
struct ntp_packet request;
memset(&request, 0, sizeof(request));
request.li_vn_mode = (0 << 6) | (4 << 3) | 3;
@ -73,43 +85,23 @@ static int ntp_do_query(const struct sockaddr_in* sin, const char* server_name,
#endif
uint64_t t1_ntp = timeval_to_ntp(&t1_tv);
request.xmit_ts = htobe64(t1_ntp);
memcpy(req, &request, sizeof(request));
if (t1_ntp_out) *t1_ntp_out = t1_ntp;
}
int send_ok = 0;
int tries;
for (tries = 0; tries < NTP_MAX_TRIES; tries++) {
ssize_t sent = sendto(sock, (const char*)&request, sizeof(request), 0,
(const struct sockaddr*)sin, sizeof(*sin));
if (sent < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: sendto(%s) failed: %s", server_name, socket_strerror(socket_get_error()));
break;
}
/* возвращает 0 если ответ валиден; иначе -1 (пакет игнорируется, ждём timeout) */
static int ntp_parse_reply(const uint8_t* buf, size_t len, uint64_t t1_ntp,
int64_t* offset_us_out, int64_t* rtt_us_out, int* stratum_out) {
if (len < sizeof(struct ntp_packet)) return -1;
const struct ntp_packet* reply = (const struct ntp_packet*)buf;
fd_set fds;
FD_ZERO(&fds);
FD_SET(sock, &fds);
struct timeval tv = {NTP_TIMEOUT_SEC, 0};
int r = select((int)(sock + 1), &fds, NULL, NULL, &tv);
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: select() failed: %s", socket_strerror(socket_get_error()));
break;
}
if (r > 0) { send_ok = 1; break; }
}
if (!send_ok) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: no response from %s after %d tries", server_name, tries);
socket_close_wrapper(sock);
return -1;
}
uint8_t mode = reply->li_vn_mode & 0x07;
if (mode != 4) return -1;
struct ntp_packet reply;
struct sockaddr_in from;
socklen_t from_len = sizeof(from);
ssize_t n = recvfrom(sock, (char*)&reply, sizeof(reply), 0, (struct sockaddr*)&from, &from_len);
if (n < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: recvfrom(%s) failed: %s", server_name, socket_strerror(socket_get_error()));
socket_close_wrapper(sock);
return -1;
}
int stratum = reply->stratum;
if (stratum == 0) return -1;
if (be64toh(reply->orig_ts) != t1_ntp) return -1;
struct timeval t4_tv;
#ifdef _WIN32
@ -117,64 +109,193 @@ static int ntp_do_query(const struct sockaddr_in* sin, const char* server_name,
#else
gettimeofday(&t4_tv, NULL);
#endif
socket_close_wrapper(sock);
int64_t t1_us = ntp64_to_us(t1_ntp);
int64_t t2_us = ntp64_to_us(be64toh(reply->recv_ts));
int64_t t3_us = ntp64_to_us(be64toh(reply->xmit_ts));
int64_t t4_us = ntp64_to_us(timeval_to_ntp(&t4_tv));
if (n < (ssize_t)sizeof(reply)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: short reply from %s (got %zd, expected %zu)", server_name, n, sizeof(reply));
return -1;
int64_t offset_us = ((t2_us - t1_us) + (t3_us - t4_us)) / 2;
int64_t rtt_us = (t4_us - t1_us) - (t3_us - t2_us);
*offset_us_out = offset_us;
*rtt_us_out = rtt_us;
*stratum_out = stratum;
return 0;
}
/* ─── машина состояний ─── */
static uint16_t ntp_query_port(const struct NTP_TIME* ntp) {
return ntp->test_port ? ntp->test_port : NTP_PORT;
}
static void ntp_try_next(struct ntp_sync* ctx) {
struct NTP_TIME* ntp = &ctx->inst->ntp;
for (int i = 0; i < ntp->server_count; i++) {
int idx = ctx->server_idx;
ctx->server_idx = (idx + 1) % ntp->server_count;
const char* name = ntp->servers[idx];
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(ntp_query_port(ntp));
if (inet_pton(AF_INET, name, &addr.sin_addr) == 1) {
ntp_start_query(ctx, idx, &addr);
return;
}
struct adns_opts dopts, *popts = NULL;
if (ntp->test_dns.sin_port != 0) {
memset(&dopts, 0, sizeof(dopts));
dopts.server = ntp->test_dns;
popts = &dopts;
}
ctx->resolving_idx = idx;
ctx->dns_q = adns_resolve(ctx->inst->ua, name, popts, ntp_resolve_done_cb, ctx);
if (ctx->dns_q) return;
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: resolve '%s' failed immediately", name);
}
uint8_t mode = reply.li_vn_mode & 0x07;
if (mode != 4) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: unexpected mode %d from %s (expected 4)", mode, server_name);
return -1;
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: all servers unreachable, retry in %ds", ntp->resync_interval_sec);
ntp_finish_cycle(ctx, 0);
}
static void ntp_resolve_done_cb(const struct adns_result* res, void* arg) {
struct ntp_sync* ctx = (struct ntp_sync*)arg;
ctx->dns_q = NULL;
struct NTP_TIME* ntp = &ctx->inst->ntp;
int idx = ctx->resolving_idx;
if (res->status == ADNS_OK && res->count > 0) {
struct sockaddr_in addr = res->addrs[0];
addr.sin_port = htons(ntp_query_port(ntp));
ntp_start_query(ctx, idx, &addr);
} else {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: resolve '%s' failed (status=%d)", ntp->servers[idx], res->status);
ntp_try_next(ctx);
}
}
int stratum = reply.stratum;
if (stratum == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: kiss-o-death from %s", server_name);
return -1;
static void ntp_start_query(struct ntp_sync* ctx, int idx, const struct sockaddr_in* addr) {
ctx->query_idx = idx;
ctx->qaddr = *addr;
ctx->tries = 0;
ntp_send_request(ctx);
}
static void ntp_send_request(struct ntp_sync* ctx) {
struct UTUN_INSTANCE* inst = ctx->inst;
if (ctx->sock == SOCKET_INVALID) {
ctx->sock = socket_create_udp(AF_INET);
if (ctx->sock == SOCKET_INVALID) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: socket() failed: %s", socket_strerror(socket_get_error()));
ntp_query_fail(ctx);
return;
}
socket_set_nonblocking(ctx->sock);
}
uint64_t reply_orig = be64toh(reply.orig_ts);
if (reply_orig != t1_ntp) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: originate timestamp mismatch from %s", server_name);
return -1;
if (ctx->tries == 0) ntp_build_request(ctx->req, &ctx->t1_ntp);
ssize_t sent = socket_sendto(ctx->sock, ctx->req, sizeof(ctx->req),
(const struct sockaddr*)&ctx->qaddr, sizeof(ctx->qaddr));
if (sent < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: sendto failed: %s", socket_strerror(socket_get_error()));
ntp_query_fail(ctx);
return;
}
int64_t t1_us = ntp64_to_us(t1_ntp);
int64_t t2_us = ntp64_to_us(be64toh(reply.recv_ts));
int64_t t3_us = ntp64_to_us(be64toh(reply.xmit_ts));
int64_t t4_us = ntp64_to_us(timeval_to_ntp(&t4_tv));
if (!ctx->socket_id) {
ctx->socket_id = uasync_add_socket_t(inst->ua, ctx->sock, ntp_query_read_cb, NULL, NULL, ctx);
if (!ctx->socket_id) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: uasync_add_socket_t failed");
ntp_query_fail(ctx);
return;
}
}
int64_t offset_us = ((t2_us - t1_us) + (t3_us - t4_us)) / 2;
int64_t rtt_us = (t4_us - t1_us) - (t3_us - t2_us);
ctx->timer = uasync_set_timeout(inst->ua, NTP_TIMEOUT_SEC * 10000, ctx, ntp_query_timeout_cb, "ntp_q");
if (!ctx->timer) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: uasync_set_timeout failed");
ntp_query_fail(ctx);
return;
}
}
static void ntp_query_read_cb(socket_t sock, void* arg) {
(void)sock;
struct ntp_sync* ctx = (struct ntp_sync*)arg;
struct ntp_packet reply;
struct sockaddr_in from;
socklen_t from_len = sizeof(from);
ssize_t n = socket_recvfrom(ctx->sock, (void*)&reply, sizeof(reply), (struct sockaddr*)&from, &from_len);
if (n < 0) return; /* EAGAIN / ICMP (WSAECONNRESET) — ждём timeout */
int64_t offset_us, rtt_us;
int stratum;
if (ntp_parse_reply((const uint8_t*)&reply, (size_t)n, ctx->t1_ntp, &offset_us, &rtt_us, &stratum) != 0)
return; /* невалидный ответ — ждём (timeout перезапросит) */
struct NTP_TIME* ntp = &ctx->inst->ntp;
ntp->offset_us = offset_us;
ntp->synced = 1;
ntp->last_sync_tb = get_time_tb();
if (ctx->query_idx >= 0)
ntp->server_current = (ctx->query_idx + 1) % ntp->server_count;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: synced from %s offset=%lldus rtt=%lldus stratum=%d",
server_name, (long long)offset_us, (long long)rtt_us, stratum);
ctx->query_idx >= 0 ? ntp->servers[ctx->query_idx] : "test-server",
(long long)offset_us, (long long)rtt_us, stratum);
*offset_us_out = offset_us;
*stratum_out = stratum;
return 0;
ntp_finish_cycle(ctx, 1);
}
static int ntp_query_server(const char* server, int64_t* offset_us_out, int* stratum_out) {
struct addrinfo hints, *result = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
char port_str[8];
snprintf(port_str, sizeof(port_str), "%d", NTP_PORT);
int gai_err = getaddrinfo(server, port_str, &hints, &result);
if (gai_err != 0 || !result) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: failed to resolve %s: %s", server, gai_strerror(gai_err));
return -1;
static void ntp_query_timeout_cb(void* arg) {
struct ntp_sync* ctx = (struct ntp_sync*)arg;
ctx->timer = NULL;
ctx->tries++;
if (ctx->tries < NTP_MAX_TRIES) {
ntp_send_request(ctx);
return;
}
ntp_query_fail(ctx);
}
static void ntp_query_fail(struct ntp_sync* ctx) {
struct NTP_TIME* ntp = &ctx->inst->ntp;
const char* name = ctx->query_idx >= 0 ? ntp->servers[ctx->query_idx] : "test-server";
if (ctx->timer) { uasync_cancel_timeout(ctx->inst->ua, ctx->timer); ctx->timer = NULL; }
if (ctx->socket_id) { uasync_remove_socket_t(ctx->inst->ua, ctx->sock); ctx->socket_id = NULL; }
if (ctx->sock != SOCKET_INVALID) { socket_close_wrapper(ctx->sock); ctx->sock = SOCKET_INVALID; }
int ret = ntp_do_query((const struct sockaddr_in*)result->ai_addr, server, offset_us_out, stratum_out);
freeaddrinfo(result);
return ret;
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: no response from %s after %d tries", name, ctx->tries);
ntp_try_next(ctx);
}
static void ntp_finish_cycle(struct ntp_sync* ctx, int synced) {
struct UTUN_INSTANCE* inst = ctx->inst;
struct NTP_TIME* ntp = &inst->ntp;
if (ctx->timer) { uasync_cancel_timeout(inst->ua, ctx->timer); ctx->timer = NULL; }
if (ctx->socket_id) { uasync_remove_socket_t(inst->ua, ctx->sock); ctx->socket_id = NULL; }
if (ctx->sock != SOCKET_INVALID) { socket_close_wrapper(ctx->sock); ctx->sock = SOCKET_INVALID; }
if (ctx->dns_q) { adns_cancel(ctx->dns_q); ctx->dns_q = NULL; }
ntp->sync = NULL;
u_free(ctx);
if (synced) {
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: time corrected, offset=%lldus", (long long)ntp->offset_us);
ntp_node_sync_peers(inst);
}
ntp->timer = uasync_set_timeout(inst->ua, ntp->resync_interval_sec * 10000,
inst, ntp_time_sync_cb, "ntp_sync");
}
static void ntp_time_sync_cb(void* arg) {
@ -189,52 +310,30 @@ static void ntp_time_sync_cb(void* arg) {
return;
}
int64_t offset_us = 0;
int stratum = 0;
int synced = 0;
if (ntp->sync) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: sync already in progress, skipping");
ntp->timer = uasync_set_timeout(instance->ua, ntp->resync_interval_sec * 10000,
instance, ntp_time_sync_cb, "ntp_sync");
return;
}
if (ntp->test_addr.sin_port != 0) {
if (ntp_do_query(&ntp->test_addr, "test-server", &offset_us, &stratum) == 0) {
ntp->offset_us = offset_us;
ntp->synced = 1;
ntp->last_sync_tb = get_time_tb();
synced = 1;
}
} else {
const char* server = ntp->servers[ntp->server_current];
if (ntp_query_server(server, &offset_us, &stratum) == 0) {
ntp->offset_us = offset_us;
ntp->synced = 1;
ntp->last_sync_tb = get_time_tb();
ntp->server_current = (ntp->server_current + 1) % ntp->server_count;
synced = 1;
} else {
for (int i = 0; i < ntp->server_count; i++) {
ntp->server_current = (ntp->server_current + 1) % ntp->server_count;
const char* next_server = ntp->servers[ntp->server_current];
if (strcmp(next_server, server) == 0) break;
if (ntp_query_server(next_server, &offset_us, &stratum) == 0) {
ntp->offset_us = offset_us;
ntp->synced = 1;
ntp->last_sync_tb = get_time_tb();
synced = 1;
break;
}
}
}
struct ntp_sync* ctx = u_calloc(1, sizeof(*ctx));
if (!ctx) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: calloc sync ctx failed");
ntp->timer = uasync_set_timeout(instance->ua, ntp->resync_interval_sec * 10000,
instance, ntp_time_sync_cb, "ntp_sync");
return;
}
ctx->inst = instance;
ctx->sock = SOCKET_INVALID;
ntp->sync = ctx;
if (ntp->synced) {
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: time corrected, offset=%lldus",
(long long)ntp->offset_us);
ntp_node_sync_peers(instance);
if (ntp->test_addr.sin_port != 0) {
ntp_start_query(ctx, -1, &ntp->test_addr);
} else {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: all servers unreachable, retry in %ds",
ntp->resync_interval_sec);
ctx->server_idx = ntp->server_current;
ntp_try_next(ctx);
}
ntp->timer = uasync_set_timeout(instance->ua, ntp->resync_interval_sec * 10000,
instance, ntp_time_sync_cb, "ntp_sync");
}
int ntp_time_init(struct UTUN_INSTANCE* instance) {
@ -251,7 +350,10 @@ int ntp_time_init(struct UTUN_INSTANCE* instance) {
ntp->server_count = g->ntp_server_count;
ntp->server_current = 0;
ntp->resync_interval_sec = g->ntp_resync_interval;
ntp->sync = NULL;
ntp->test_port = 0;
memset(&ntp->test_addr, 0, sizeof(ntp->test_addr));
memset(&ntp->test_dns, 0, sizeof(ntp->test_dns));
if (g->ntp_server_count > 0) {
ntp->servers = u_malloc(g->ntp_server_count * sizeof(char*));
@ -298,6 +400,16 @@ void ntp_time_destroy(struct UTUN_INSTANCE* instance) {
if (!instance) return;
struct NTP_TIME* ntp = &instance->ntp;
if (ntp->sync && instance->ua) {
struct ntp_sync* ctx = (struct ntp_sync*)ntp->sync;
if (ctx->timer) { uasync_cancel_timeout(instance->ua, ctx->timer); ctx->timer = NULL; }
if (ctx->socket_id) { uasync_remove_socket_t(instance->ua, ctx->sock); ctx->socket_id = NULL; }
if (ctx->sock != SOCKET_INVALID) socket_close_wrapper(ctx->sock);
if (ctx->dns_q) adns_cancel(ctx->dns_q);
u_free(ctx);
ntp->sync = NULL;
}
if (ntp->timer && instance->ua) {
uasync_cancel_timeout(instance->ua, ntp->timer);
ntp->timer = NULL;
@ -350,3 +462,16 @@ void ntp_time_set_test_addr(struct NTP_TIME* ntp, const char* ip, uint16_t port)
memset(&ntp->test_addr, 0, sizeof(ntp->test_addr));
}
}
void ntp_time_set_test_dns(struct NTP_TIME* ntp, const char* ip, uint16_t dns_port, uint16_t ntp_port) {
if (!ntp || !ip) return;
memset(&ntp->test_dns, 0, sizeof(ntp->test_dns));
ntp->test_dns.sin_family = AF_INET;
ntp->test_dns.sin_port = htons(dns_port);
ntp->test_port = ntp_port;
if (inet_pton(AF_INET, ip, &ntp->test_dns.sin_addr) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: test dns parse failed for %s", ip);
memset(&ntp->test_dns, 0, sizeof(ntp->test_dns));
ntp->test_port = 0;
}
}

4
src/ntp_time.h

@ -25,12 +25,16 @@ struct NTP_TIME {
int server_current;
int resync_interval_sec;
struct sockaddr_in test_addr; // test mode: direct address when sin_port != 0
struct sockaddr_in test_dns; // test mode: DNS server for hostname resolution (sin_port != 0)
uint16_t test_port; // test mode: NTP query port override (0 = 123)
void* sync; // активный контекст синхронизации (struct ntp_sync*, opaque)
};
int ntp_time_init(struct UTUN_INSTANCE* instance);
void ntp_time_destroy(struct UTUN_INSTANCE* instance);
void ntp_time_set_test_addr(struct NTP_TIME* ntp, const char* ip, uint16_t port);
void ntp_time_set_test_dns(struct NTP_TIME* ntp, const char* ip, uint16_t dns_port, uint16_t ntp_port);
int64_t ntp_time_get_us(struct UTUN_INSTANCE* instance);
time_t ntp_time_get_seconds(struct UTUN_INSTANCE* instance);

11
tests/Makefile.am

@ -64,6 +64,7 @@ check_PROGRAMS = \
test_tcp_io \
test_uasync_socket_race \
test_ntp \
test_async_dns \
test_opus_codec \
test_media_async \
test_media_index \
@ -96,12 +97,12 @@ else
WIN_LIBS =
endif
# Common libraries
COMMON_LIBS = $(top_builddir)/lib/libuasync.a -lpthread $(WIN_LIBS)
# Crypto always uses OpenSSL
CRYPTO_LIBS = -lcrypto
# Common libraries (lib/dns.c собирается с -DDNS_RANDOM=RAND_bytes → нужен -lcrypto)
COMMON_LIBS = $(top_builddir)/lib/libuasync.a $(CRYPTO_LIBS) -lpthread $(WIN_LIBS)
# Test definitions
test_etcp_bbr_SOURCES = test_etcp_bbr.c
test_etcp_bbr_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
@ -254,6 +255,10 @@ 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)
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)
test_memory_pool_and_config_SOURCES = test_memory_pool_and_config.c
test_memory_pool_and_config_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_memory_pool_and_config_LDADD = $(COMMON_LIBS)

291
tests/test_async_dns.c

@ -0,0 +1,291 @@
/**
* @file test_async_dns.c
* @brief Юнит-тесты async_dns (асинхронный DNS-резолвер поверх libdns).
*
* 1. Чистый парсер resolv.conf (adns_parse_resolv_conf).
* 2. Сквозные тесты с фейковым DNS-сервером в отдельном потоке:
* - A-запись (с компрессией имени);
* - два A;
* - CNAME + A;
* - NXDOMAIN;
* - таймаут (сервер молчит);
* - отмена (коллбэк не вызывается).
*/
#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 "../lib/async_dns.h"
#define SERVER_WAIT_MS 5000
#define RESOLVE_TIMEOUT_TB 5000 /* 500 ms для быстрых ответов */
#define TIMEOUT_TEST_TB 20000 /* 2 s (timeout_ms=1000, attempts=1 → ждём > 1 s) */
static volatile int g_server_running = 1;
static volatile uint16_t g_server_port = 0;
static void platform_sleep_ms(int ms) {
#ifdef _WIN32
Sleep(ms);
#else
usleep(ms * 1000);
#endif
}
/* собрать qname (завершается точкой) из query по смещению 12 */
static void extract_qname(const uint8_t* q, int n, char* out, int cap) {
int p = 12, l = 0;
while (p < n && q[p] != 0 && l < cap - 1) {
if ((q[p] & 0xC0) == 0xC0) { p += 2; break; }
int lab = q[p++];
for (int j = 0; j < lab && p < n && l < cap - 1; j++) out[l++] = (char)q[p++];
out[l++] = '.';
}
out[l] = '\0';
}
/* конец question-секции (после qname + qtype + qclass) */
static int question_end(const uint8_t* q, int n) {
int p = 12;
while (p < n && q[p] != 0) {
if ((q[p] & 0xC0) == 0xC0) { return p + 2 + 4; }
p += 1 + q[p];
}
return p + 1 + 4;
}
/* собрать DNS-ответ: qid + вопрос (из query) + ответы. rcode и ancount задаются. */
static int build_response(const uint8_t* q, int n, uint8_t* out, int cap,
int rcode, int ancount,
const uint16_t* atype, const uint8_t** adata, const uint16_t* alen) {
int qend = question_end(q, n);
if (qend > n) return -1;
int o = 0;
out[o++] = q[0]; out[o++] = q[1]; /* qid */
uint16_t flags = (uint16_t)(0x8180 | rcode); /* QR + RD + RA + rcode */
out[o++] = (uint8_t)(flags >> 8); out[o++] = (uint8_t)(flags & 0xFF);
out[o++] = 0; out[o++] = 1; /* qdcount */
out[o++] = 0; out[o++] = (uint8_t)ancount; /* ancount */
out[o++] = 0; out[o++] = 0; /* nscount */
out[o++] = 0; out[o++] = 0; /* arcount */
memcpy(out + o, q + 12, (size_t)(qend - 12)); /* question */
o += qend - 12;
for (int i = 0; i < ancount && o + 12 + alen[i] <= cap; i++) {
out[o++] = 0xC0; out[o++] = 0x0C; /* имя = указатель на вопрос */
out[o++] = (uint8_t)(atype[i] >> 8); out[o++] = (uint8_t)(atype[i] & 0xFF);
out[o++] = 0; out[o++] = 1; /* class IN */
out[o++] = 0; out[o++] = 0; out[o++] = 0; out[o++] = 60; /* ttl */
out[o++] = (uint8_t)(alen[i] >> 8); out[o++] = (uint8_t)(alen[i] & 0xFF);
memcpy(out + o, adata[i], alen[i]); o += alen[i];
}
return o;
}
static void* dns_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 al = sizeof(addr);
getsockname(sock, (struct sockaddr*)&addr, &al);
g_server_port = ntohs(addr.sin_port);
uint8_t buf[512];
while (g_server_running) {
fd_set fds; FD_ZERO(&fds); FD_SET(sock, &fds);
struct timeval tv = {1, 0};
int r = select((int)(sock + 1), &fds, NULL, NULL, &tv);
if (r <= 0) continue;
struct sockaddr_in client; socklen_t cl = sizeof(client);
ssize_t n = recvfrom(sock, (char*)buf, sizeof(buf), 0, (struct sockaddr*)&client, &cl);
if (n < 12) continue;
char qname[256];
extract_qname(buf, (int)n, qname, sizeof(qname));
uint8_t resp[512]; int rl = 0;
if (strcmp(qname, "a.test.") == 0) {
uint8_t ip[4]; inet_pton(AF_INET, "192.168.1.42", ip);
const uint8_t* d = ip; uint16_t l = 4;
rl = build_response(buf, (int)n, resp, sizeof(resp), 0, 1, (const uint16_t[]){1}, &d, &l);
} else if (strcmp(qname, "multi.test.") == 0) {
uint8_t ip1[4]; inet_pton(AF_INET, "10.0.0.1", ip1);
uint8_t ip2[4]; inet_pton(AF_INET, "10.0.0.2", ip2);
const uint8_t* d[2] = { ip1, ip2 }; uint16_t l[2] = {4, 4};
rl = build_response(buf, (int)n, resp, sizeof(resp), 0, 2, (const uint16_t[]){1, 1}, d, l);
} else if (strcmp(qname, "cname.test.") == 0) {
/* CNAME → real.test., затем A */
uint8_t cname[16] = { 4, 'r','e','a','l', 4, 't','e','s','t', 0 };
uint8_t ip[4]; inet_pton(AF_INET, "192.168.1.99", ip);
const uint8_t* d[2] = { cname, ip }; uint16_t l[2] = {11, 4};
rl = build_response(buf, (int)n, resp, sizeof(resp), 0, 2, (const uint16_t[]){5, 1}, d, l);
} else if (strcmp(qname, "nx.test.") == 0) {
rl = build_response(buf, (int)n, resp, sizeof(resp), 3, 0, NULL, NULL, NULL);
} else if (strcmp(qname, "silent.test.") == 0) {
continue; /* молчим — таймаут */
} else {
continue; /* неизвестное имя — молчим */
}
if (rl > 0)
sendto(sock, (const char*)resp, rl, 0, (struct sockaddr*)&client, sizeof(client));
}
socket_close_wrapper(sock);
return NULL;
}
struct test_ctx {
int done;
struct adns_result res;
};
static void on_done(const struct adns_result* res, void* arg) {
struct test_ctx* t = (struct test_ctx*)arg;
t->res = *res;
t->done = 1;
}
/* запустить резолв и дождаться коллбэка или таймаута. Возвращает 0=успех. */
static int run_resolve(struct UASYNC* ua, const char* name, struct adns_opts* opts,
struct test_ctx* t, uint64_t timeout_tb) {
memset(t, 0, sizeof(*t));
struct adns_query* q = adns_resolve(ua, name, opts, on_done, t);
if (!q) return -1;
uint64_t start = get_time_tb();
while (!t->done && get_time_tb() - start < timeout_tb)
uasync_poll(ua, 10);
if (!t->done) { adns_cancel(q); return -2; }
return 0;
}
#define ASSERT(c, msg) do { if (!(c)) { \
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: %s", msg); goto fail; } } while (0)
int main(void) {
int result = 1;
struct UASYNC* ua = NULL;
pthread_t srv;
int srv_started = 0;
debug_config_init();
debug_set_level(DEBUG_LEVEL_WARN);
debug_set_category_level(DEBUG_CATEGORY_SOCKET, DEBUG_LEVEL_INFO);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "=== async_dns test ===");
/* ── 1. чистый парсер resolv.conf ── */
{
struct sockaddr_in out[4];
const char* text = "# comment\nnameserver 8.8.8.8\nnameserver 1.1.1.1 # inline\nsearch foo\n";
int n = adns_parse_resolv_conf(text, out, 4);
ASSERT(n == 2, "parse_resolv_conf count");
char ip[16];
inet_ntop(AF_INET, &out[0].sin_addr, ip, sizeof(ip));
ASSERT(strcmp(ip, "8.8.8.8") == 0, "parse_resolv_conf[0] == 8.8.8.8");
inet_ntop(AF_INET, &out[1].sin_addr, ip, sizeof(ip));
ASSERT(strcmp(ip, "1.1.1.1") == 0, "parse_resolv_conf[1] == 1.1.1.1");
ASSERT(ntohs(out[0].sin_port) == 53, "parse_resolv_conf port 53");
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "PASS: parse_resolv_conf");
/* ── 2. фейковый DNS-сервер ── */
if (pthread_create(&srv, NULL, dns_server_thread, NULL) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "pthread_create failed");
return 1;
}
srv_started = 1;
uint64_t wstart = get_time_tb();
while (g_server_port == 0 && get_time_tb() - wstart < (uint64_t)(SERVER_WAIT_MS * 10))
platform_sleep_ms(5);
if (g_server_port == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "fake DNS server did not bind");
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "fake DNS server on 127.0.0.1:%u", g_server_port);
ua = uasync_create();
if (!ua) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "uasync_create failed");
goto cleanup;
}
struct adns_opts opts;
memset(&opts, 0, sizeof(opts));
opts.server.sin_family = AF_INET;
opts.server.sin_addr.s_addr = inet_addr("127.0.0.1");
opts.server.sin_port = htons(g_server_port);
opts.timeout_ms = 1000;
opts.max_attempts = 1;
struct test_ctx t;
char ip[16];
/* A-запись */
ASSERT(run_resolve(ua, "a.test", &opts, &t, RESOLVE_TIMEOUT_TB) == 0, "a.test resolve completes");
ASSERT(t.res.status == ADNS_OK, "a.test status OK");
ASSERT(t.res.count == 1, "a.test count == 1");
inet_ntop(AF_INET, &t.res.addrs[0].sin_addr, ip, sizeof(ip));
ASSERT(strcmp(ip, "192.168.1.42") == 0, "a.test addr == 192.168.1.42");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "PASS: A record (compression)");
/* два A */
ASSERT(run_resolve(ua, "multi.test", &opts, &t, RESOLVE_TIMEOUT_TB) == 0, "multi resolve");
ASSERT(t.res.status == ADNS_OK && t.res.count == 2, "multi count == 2");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "PASS: multiple A");
/* CNAME + A */
ASSERT(run_resolve(ua, "cname.test", &opts, &t, RESOLVE_TIMEOUT_TB) == 0, "cname resolve");
ASSERT(t.res.status == ADNS_OK && t.res.count >= 1, "cname status OK");
inet_ntop(AF_INET, &t.res.addrs[0].sin_addr, ip, sizeof(ip));
ASSERT(strcmp(ip, "192.168.1.99") == 0, "cname addr == 192.168.1.99");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "PASS: CNAME + A");
/* NXDOMAIN */
ASSERT(run_resolve(ua, "nx.test", &opts, &t, RESOLVE_TIMEOUT_TB) == 0, "nx resolve");
ASSERT(t.res.status == ADNS_ERR_NODATA, "nx status NODATA");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "PASS: NXDOMAIN");
/* таймаут: сервер молчит */
ASSERT(run_resolve(ua, "silent.test", &opts, &t, TIMEOUT_TEST_TB) == 0, "silent resolve");
ASSERT(t.res.status == ADNS_ERR_TIMEOUT, "silent status TIMEOUT");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "PASS: timeout");
/* отмена: коллбэк не вызывается */
{
memset(&t, 0, sizeof(t));
struct adns_query* q = adns_resolve(ua, "silent.test", &opts, on_done, &t);
ASSERT(q != NULL, "cancel: resolve created");
for (int i = 0; i < 50 && !t.done; i++) uasync_poll(ua, 10); /* немного покрутим */
adns_cancel(q);
for (int i = 0; i < 50; i++) uasync_poll(ua, 10);
ASSERT(t.done == 0, "cancel: callback NOT invoked");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "PASS: cancel");
}
result = 0;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "=== async_dns test PASSED ===");
goto cleanup;
fail:
result = 1;
cleanup:
if (ua) uasync_destroy(ua, 0);
if (srv_started) { g_server_running = 0; pthread_join(srv, NULL); }
return result;
}

265
tests/test_ntp.c

@ -1,10 +1,11 @@
/**
* @file test_ntp.c
* @brief NTP client test with local fake NTP server
* @brief NTP client test: direct address + hostname (async DNS) paths
*
* 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().
* Сценарий 1 (direct): фейковый NTP-сервер, синхронизация по test_addr.
* Сценарий 2 (hostname): фейковый DNS-сервер резолвит hostname → 127.0.0.1,
* затем NTP опрашивает фейковый NTP-сервер (async_dns → неблокирующий запрос).
* Оба сценария проверяют флаг synced и точность offset.
*/
#include <stdio.h>
#include <stdlib.h>
@ -46,12 +47,23 @@ _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 volatile int g_dns_running = 1;
static volatile uint16_t g_dns_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 platform_sleep_ms(int ms) {
#ifdef _WIN32
Sleep(ms);
#else
usleep(ms * 1000);
#endif
}
static void* test_server_thread(void* arg) {
(void)arg;
socket_t sock = socket_create_udp(AF_INET);
@ -107,53 +119,78 @@ static void* test_server_thread(void* arg) {
return NULL;
}
static void platform_sleep_ms(int ms) {
#ifdef _WIN32
Sleep(ms);
#else
usleep(ms * 1000);
#endif
}
/* ─── фейковый DNS-сервер: любой A-запрос → 127.0.0.1 ─── */
static int wait_for_port(void) {
for (int i = 0; i < 200; i++) {
if (g_server_port != 0) return 0;
platform_sleep_ms(5);
static int dns_question_end(const uint8_t* q, int n) {
int p = 12;
while (p < n && q[p] != 0) {
if ((q[p] & 0xC0) == 0xC0) return p + 2 + 4;
p += 1 + q[p];
}
return -1;
return p + 1 + 4;
}
int main(void) {
int result = 1;
debug_config_init();
debug_set_level(DEBUG_LEVEL_WARN);
debug_set_categories(DEBUG_CATEGORY_GENERAL);
static int build_dns_a_response(const uint8_t* q, int n, uint8_t* out, int cap) {
int qend = dns_question_end(q, n);
if (qend > n) return -1;
uint8_t ip[4];
inet_pton(AF_INET, "127.0.0.1", ip);
int o = 0;
out[o++] = q[0]; out[o++] = q[1]; /* qid */
out[o++] = 0x81; out[o++] = 0x80; /* QR+RD+RA, rcode 0 */
out[o++] = 0; out[o++] = 1; /* qdcount */
out[o++] = 0; out[o++] = 1; /* ancount */
out[o++] = 0; out[o++] = 0; /* nscount */
out[o++] = 0; out[o++] = 0; /* arcount */
memcpy(out + o, q + 12, (size_t)(qend - 12)); /* question */
o += qend - 12;
out[o++] = 0xC0; out[o++] = 0x0C; /* имя = указатель на вопрос */
out[o++] = 0; out[o++] = 1; /* type A */
out[o++] = 0; out[o++] = 1; /* class IN */
out[o++] = 0; out[o++] = 0; out[o++] = 0; out[o++] = 60; /* ttl */
out[o++] = 0; out[o++] = 4; /* rdlength */
memcpy(out + o, ip, 4); o += 4;
return o;
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "=== NTP Test ===");
static void* dns_server_thread(void* arg) {
(void)arg;
socket_t sock = socket_create_udp(AF_INET);
if (sock == SOCKET_INVALID) { g_dns_running = 0; return NULL; }
/* ── 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;
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_dns_running = 0; return NULL; }
socklen_t al = sizeof(addr);
getsockname(sock, (struct sockaddr*)&addr, &al);
g_dns_port = ntohs(addr.sin_port);
uint8_t buf[512];
while (g_dns_running) {
fd_set fds; FD_ZERO(&fds); FD_SET(sock, &fds);
struct timeval tv = {1, 0};
int r = select((int)(sock + 1), &fds, NULL, NULL, &tv);
if (r <= 0) continue;
struct sockaddr_in client; socklen_t cl = sizeof(client);
ssize_t n = recvfrom(sock, (char*)buf, sizeof(buf), 0, (struct sockaddr*)&client, &cl);
if (n < 12) continue;
uint8_t resp[512];
int rl = build_dns_a_response(buf, (int)n, resp, sizeof(resp));
if (rl > 0) sendto(sock, (const char*)resp, rl, 0, (struct sockaddr*)&client, sizeof(client));
}
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; }
socket_close_wrapper(sock);
return NULL;
}
utun_instance_set_tun_init_enabled(0);
/* ─── helpers ─── */
/* Parse config from buffer and inject keys (avoid config_ensure_keys_and_node_id rewriting) */
const char *config_text =
static struct UTUN_INSTANCE* make_instance(struct UASYNC* ua, const char* server_name) {
char config_text[512];
snprintf(config_text, sizeof(config_text),
"[global]\n"
"my_node_name=ntp_test\n"
"my_private_key=e8d4d14943ca3732d7802d4010ea4f6f8bdb71e2304f78d68f4f3dc31e760c79\n"
@ -162,97 +199,105 @@ int main(void) {
"\n"
"[ntp]\n"
"enabled=yes\n"
"server=placeholder\n"
"interval=3600\n";
"server=%s\n"
"interval=3600\n",
server_name);
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);
if (!cfg) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "parse config failed"); return NULL; }
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. */
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "create instance failed"); free_config(cfg); return NULL; }
utun_instance_init(inst);
return 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 ── */
static int verify_synced(struct UTUN_INSTANCE* inst, struct UASYNC* ua, const char* label) {
uint64_t start_tb = get_time_tb();
while (get_time_tb() - start_tb < (uint64_t)(TEST_TIMEOUT_SEC * 10000)) {
while (!inst->ntp.synced && 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_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;
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);
return -1;
}
if (!ntp_time_is_synced(inst)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: %s ntp_time_is_synced returned 0", label);
return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "PASS: %s offset=%lldus", label, (long long)inst->ntp.offset_us);
return 0;
}
int main(void) {
int result = 1;
pthread_t server_thread, dns_thread;
int server_started = 0, dns_started = 0;
debug_config_init();
debug_set_level(DEBUG_LEVEL_WARN);
debug_set_categories(DEBUG_CATEGORY_GENERAL);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "=== NTP Test ===");
utun_instance_set_tun_init_enabled(0);
/* fake NTP server */
if (pthread_create(&server_thread, NULL, test_server_thread, NULL) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Failed to create server thread");
return 1;
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP synced, offset=%lldus", (long long)inst->ntp.offset_us);
server_started = 1;
{ uint64_t w = get_time_tb(); while (g_server_port == 0 && get_time_tb() - w < 50000) platform_sleep_ms(5); }
if (g_server_port == 0) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "fake NTP server did not bind"); goto cleanup; }
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Fake NTP server on 127.0.0.1:%u, offset=+%ds", g_server_port, TEST_KNOWN_OFFSET_SEC);
// Check offset is close to expected (formula gives server - client ≈ KNOWN_OFFSET)
/* ── scenario 1: direct address (test_addr) ── */
{
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;
}
struct UASYNC* ua = uasync_create();
if (!ua) goto cleanup;
struct UTUN_INSTANCE* inst = make_instance(ua, "placeholder");
if (!inst) { uasync_destroy(ua, 0); goto cleanup; }
ntp_time_set_test_addr(&inst->ntp, "127.0.0.1", g_server_port);
int ok = verify_synced(inst, ua, "direct (test_addr)");
utun_instance_destroy(inst);
uasync_destroy(ua, 0);
if (ok != 0) 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");
/* ── scenario 2: hostname resolved via fake DNS (async_dns) ── */
if (pthread_create(&dns_thread, NULL, dns_server_thread, NULL) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Failed to create DNS server thread");
goto cleanup;
}
dns_started = 1;
{ uint64_t w = get_time_tb(); while (g_dns_port == 0 && get_time_tb() - w < 50000) platform_sleep_ms(5); }
if (g_dns_port == 0) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "fake DNS server did not bind"); goto cleanup; }
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Fake DNS server on 127.0.0.1:%u", g_dns_port);
// 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;
}
struct UASYNC* ua = uasync_create();
if (!ua) goto cleanup;
struct UTUN_INSTANCE* inst = make_instance(ua, "ntp.test.local");
if (!inst) { uasync_destroy(ua, 0); goto cleanup; }
ntp_time_set_test_dns(&inst->ntp, "127.0.0.1", g_dns_port, g_server_port);
int ok = verify_synced(inst, ua, "hostname (async DNS)");
utun_instance_destroy(inst);
uasync_destroy(ua, 0);
if (ok != 0) 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);
if (server_started) { g_server_running = 0; pthread_join(server_thread, NULL); }
if (dns_started) { g_dns_running = 0; pthread_join(dns_thread, NULL); }
return result;
}

Loading…
Cancel
Save