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.
503 lines
18 KiB
503 lines
18 KiB
/* |
|
* 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> |
|
#include <ctype.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; |
|
int fast; /* 1 = готовый /etc/hosts-ответ, без libdns */ |
|
struct adns_result fast_result; |
|
void* call_soon_id; /* отложенный вызов (fast-путь и первый драйв slow-пути) */ |
|
}; |
|
|
|
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); |
|
static void adns_fast_ready_cb(void* arg); |
|
static int adns_hosts_lookup(const char* name, struct sockaddr_in* out); |
|
|
|
/* ─── 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.String; |
|
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 |
|
|
|
/* ─── синхронный /etc/hosts-лукап (быстрый путь, без libdns) ─── */ |
|
|
|
// Ищет name в /etc/hosts. Возвращает 1 если найден IPv4-адрес (out заполнен). |
|
// Блокирующее чтение небольшого файла — только для локальных имён (localhost и т.п.). |
|
static int adns_hosts_lookup(const char* name, struct sockaddr_in* out) { |
|
#ifndef _WIN32 |
|
if (!name || !out) return 0; |
|
FILE* f = fopen("/etc/hosts", "r"); |
|
if (!f) return 0; |
|
char line[512]; |
|
int found = 0; |
|
while (!found && fgets(line, sizeof(line), f)) { |
|
char ip[64]; int n = 0; |
|
const char* p = line; |
|
while (*p && !isspace(*p) && n < 63) ip[n++] = *p++; |
|
ip[n] = '\0'; |
|
if (!ip[0] || ip[0] == '#') continue; |
|
struct in_addr a; |
|
if (inet_pton(AF_INET, ip, &a) != 1) continue; |
|
while (*p && isspace(*p)) p++; |
|
while (*p && *p != '\n') { |
|
if (*p == '#') break; |
|
char h[256]; int hn = 0; |
|
while (*p && !isspace(*p) && *p != '#' && hn < 255) h[hn++] = *p++; |
|
h[hn] = '\0'; |
|
if (h[0] && strcasecmp(h, name) == 0) { |
|
memset(out, 0, sizeof(*out)); |
|
out->sin_family = AF_INET; |
|
out->sin_addr = a; |
|
found = 1; |
|
break; |
|
} |
|
while (*p && isspace(*p)) p++; |
|
} |
|
} |
|
fclose(f); |
|
return found; |
|
#else |
|
(void)name; (void)out; |
|
return 0; |
|
#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; |
|
} |
|
#ifndef _WIN32 |
|
// Локальные имена из /etc/hosts (localhost и т.п.). Не фатально, если файла нет. |
|
if (dns_hosts_loadpath(hosts, "/etc/hosts") != 0) |
|
DEBUG_DEBUG(ADNS_DEBUG_CAT, "adns: /etc/hosts not loaded"); |
|
#endif |
|
|
|
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, "dns_query", 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); |
|
} |
|
|
|
// Отложенный вызов коллбэка для fast-пути (/etc/hosts): сохраняет async-контракт |
|
// (adns_resolve возвращает handle, коллбэк вызывается в потоке event loop). |
|
static void adns_fast_ready_cb(void* arg) { |
|
struct adns_query* q = (struct adns_query*)arg; |
|
q->call_soon_id = NULL; |
|
if (q->finished) return; // отменён до срабатывания |
|
q->finished = 1; |
|
adns_done_cb cb = q->cb; |
|
void* a = q->arg; |
|
struct adns_result res = q->fast_result; |
|
char* nm = q->name; |
|
q->name = NULL; |
|
u_free(q); |
|
if (cb) cb(&res, a); |
|
u_free(nm); |
|
} |
|
|
|
// Отложенный первый драйв slow-пути (libdns): adns_resolve возвращает живой handle, |
|
// а реальный запуск запроса происходит в event loop. Это исключает ситуацию, когда |
|
// dns_res_check завершается синхронно прямо внутри adns_resolve() и освобождает q |
|
// до того, как вызывающий код получил и сохранил handle. |
|
static void adns_drive_trampoline(void* arg) { |
|
struct adns_query* q = (struct adns_query*)arg; |
|
q->call_soon_id = NULL; |
|
if (q->finished) return; // отменён до срабатывания |
|
adns_drive(q); |
|
} |
|
|
|
/* ─── 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; } |
|
|
|
// Быстрый путь: имя из /etc/hosts (localhost и т.п.) — без libdns и сети. |
|
// libdns по умолчанию идёт в DNS первым (lookup="bf"), поэтому локальные имена |
|
// ждали бы таймаут DNS-запроса (2×2s); обрабатываем их сразу. |
|
{ |
|
struct sockaddr_in ha; |
|
if (adns_hosts_lookup(name, &ha)) { |
|
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->name = u_strdup(name); |
|
if (!q->name) { u_free(q); DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: strdup failed"); return NULL; } |
|
q->fast = 1; |
|
q->fast_result.status = ADNS_OK; |
|
q->fast_result.count = 1; |
|
q->fast_result.addrs[0] = ha; |
|
q->call_soon_id = uasync_call_soon(ua, q, adns_fast_ready_cb); |
|
if (!q->call_soon_id) { u_free(q->name); u_free(q); return NULL; } |
|
DEBUG_DEBUG(ADNS_DEBUG_CAT, "adns: '%s' resolved from /etc/hosts (fast)", name); |
|
return q; |
|
} |
|
} |
|
|
|
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); |
|
|
|
q->call_soon_id = uasync_call_soon(ua, q, adns_drive_trampoline); |
|
if (!q->call_soon_id) { |
|
DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: uasync_call_soon failed for '%s'", name); |
|
dns_res_close(q->res); |
|
u_free(q->name); |
|
u_free(q); |
|
return NULL; |
|
} |
|
return q; |
|
} |
|
|
|
void adns_cancel(struct adns_query* q) { |
|
if (!q || q->finished) return; |
|
q->finished = 1; |
|
if (q->call_soon_id) { uasync_call_soon_cancel(q->ua, q->call_soon_id); q->call_soon_id = NULL; } |
|
if (q->fast) { |
|
u_free(q->name); |
|
u_free(q); |
|
return; |
|
} |
|
adns_teardown(q); |
|
u_free(q->name); |
|
u_free(q); |
|
}
|
|
|