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.
291 lines
12 KiB
291 lines
12 KiB
/** |
|
* @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; |
|
}
|
|
|