Browse Source
- Вендор 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
10 changed files with 11478 additions and 246 deletions
@ -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); |
||||
} |
||||
@ -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 */ |
||||
@ -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; |
||||
} |
||||
Loading…
Reference in new issue