diff --git a/BUILD.bazel b/BUILD.bazel index 1a5374565..1a229bde3 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -294,6 +294,7 @@ SSL_HEADERS = [ EVENT_HEADERS = [ "event/hloop.h", "event/nlog.h", + "event/hdns.h", ] UTIL_HEADERS = [ diff --git a/Makefile b/Makefile index f6fa6e583..2eafb2a15 100644 --- a/Makefile +++ b/Makefile @@ -69,6 +69,7 @@ EXAMPLES = hmain_test htimer_test hloop_test pipe_test \ udp_echo_server \ udp_proxy_server \ socks5_proxy_server \ + host \ multi-acceptor-processes \ multi-acceptor-threads \ one-acceptor-multi-workers \ @@ -172,6 +173,9 @@ udp_proxy_server: prepare socks5_proxy_server: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/socks5_proxy_server.c" +host: prepare + $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/host.c" + multi-acceptor-processes: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/multi-thread/multi-acceptor-processes.c" @@ -305,7 +309,12 @@ unittest: prepare $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Iprotocol -o bin/ping unittest/ping_test.c protocol/icmp.c base/hsocket.c base/htime.c -DPRINT_DEBUG $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Iprotocol -o bin/ftp unittest/ftp_test.c protocol/ftp.c base/hsocket.c base/htime.c $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Iprotocol -Iutil -o bin/sendmail unittest/sendmail_test.c protocol/smtp.c base/hsocket.c base/htime.c util/base64.c + $(MAKE) libhv + $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Issl -Ievent -o bin/hdns_test unittest/hdns_test.c -Llib -lhv -pthread + $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Issl -Ievent -o bin/hdns_benchmark unittest/hdns_benchmark.c -Llib -lhv -pthread ifeq ($(WITH_EVPP), yes) + $(MAKE) libhv + $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -o bin/tcpclient_dns_test unittest/tcpclient_dns_test.cpp -Llib -lhv -pthread ifeq ($(WITH_REDIS), yes) $(MAKE) libhv $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Ievent -Icpputil -Iredis -o bin/redis_protocol_test unittest/redis_protocol_test.cpp redis/RedisMessage.cpp @@ -317,6 +326,7 @@ else $(RM) bin/redis_protocol_test bin/redis_async_client_test bin/redis_client_test bin/redis_batch_test bin/redis_subscriber_test endif else + $(RM) bin/tcpclient_dns_test $(RM) bin/redis_protocol_test bin/redis_async_client_test bin/redis_client_test bin/redis_batch_test bin/redis_subscriber_test endif diff --git a/Makefile.vars b/Makefile.vars index 1760c1e8b..e3cb3121e 100644 --- a/Makefile.vars +++ b/Makefile.vars @@ -30,6 +30,7 @@ SSL_HEADERS = ssl/hssl.h EVENT_HEADERS = event/hloop.h\ event/nlog.h\ + event/hdns.h\ UTIL_HEADERS = util/base64.h\ util/md5.h\ diff --git a/base/herr.h b/base/herr.h index adb6d07b9..5d07f54d0 100644 --- a/base/herr.h +++ b/base/herr.h @@ -76,6 +76,7 @@ F(-1019, SENDTO, "sendto() error") \ F(-1020, SETSOCKOPT, "setsockopt() error") \ F(-1021, GETSOCKOPT, "getsockopt() error") \ + F(-1022, DNS_RESOLVE,"DNS resolve failed") \ // grpc [4xxx] #define FOREACH_ERR_GRPC(F) \ diff --git a/cmake/vars.cmake b/cmake/vars.cmake index c3b0451c6..466ab0055 100644 --- a/cmake/vars.cmake +++ b/cmake/vars.cmake @@ -27,6 +27,7 @@ set(SSL_HEADERS set(EVENT_HEADERS event/hloop.h event/nlog.h + event/hdns.h ) set(UTIL_HEADERS diff --git a/docs/PLAN.md b/docs/PLAN.md index f589d57f8..97004d020 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -9,10 +9,10 @@ - websocket client/server - mqtt client - redis client +- async DNS ## Plan -- async DNS - lua binding - js binding - hrpc = libhv + protobuf diff --git a/docs/cn/README.md b/docs/cn/README.md index 0f676b4ae..7f4c8d3a2 100644 --- a/docs/cn/README.md +++ b/docs/cn/README.md @@ -1,6 +1,7 @@ ## c接口 - [hloop: 事件循环](hloop.md) +- [hdns: 异步DNS解析](hdns.md) - [hbase: 基础函数](hbase.md) - [hlog: 日志](hlog.md) diff --git a/docs/cn/hdns.md b/docs/cn/hdns.md new file mode 100644 index 000000000..831345a9b --- /dev/null +++ b/docs/cn/hdns.md @@ -0,0 +1,189 @@ +# hdns: 异步DNS解析 + +`hdns` 是 `libhv` 内置的**异步 DNS 解析器**,完全运行在事件循环(hloop)之内。 + +## 为什么需要它 + +`libhv` 是一个事件循环库,但此前域名解析走的是**阻塞的** `getaddrinfo()`(`base/hsocket.c` 的 `ResolveAddr`)。在事件循环里同步解析 DNS 会**卡住整个 loop**——期间所有其它连接的读写、定时器都会停摆,网络抖动或 DNS 不可达时甚至会阻塞数秒。 + +`hdns` 参考 `libevent` 的 `evdns` 思路,**自己实现了一套原生的、基于非阻塞 UDP 的 DNS 解析器**:构造 DNS 报文、通过 hloop 的非阻塞 UDP 收发、解析响应、处理超时/重试,全程不阻塞事件循环,也**不引入任何第三方依赖**(不同于 libuv 的线程池方案,也不同于接入 c-ares)。 + +> 注意:`hdns` 与 `protocol/dns.*` 相互独立。后者是早期的同步 demo(仅供学习),`hdns` 自带一套完整、独立的 DNS 报文实现。 + +`hdns` 属于 `event` 核心模块,**默认编入**,无需额外编译开关。对外头文件:`event/hdns.h`。 + +## 特性 + +- 异步 A(IPv4)/ AAAA(IPv6)查询,纯事件驱动。 +- 自动读取系统 nameserver: + - Unix/Linux/macOS:解析 `/etc/resolv.conf`; + - Windows:`GetAdaptersAddresses()`(链接 `iphlpapi`); + - 任意平台若拿不到 nameserver,统一回退到 `8.8.8.8`。 +- 加载 `/etc/hosts`(Windows 为 `%SystemRoot%\System32\drivers\etc\hosts`),查询前先查表,`localhost` 等本地映射行为与系统一致。 +- 数字 IP 快速路径(字面量 IPv4/IPv6 不发起查询)。 +- 每次查询支持超时、重试、多 nameserver 轮转。 +- DNS 名字压缩解析。 +- **尊重 TTL 的进程内缓存**(正向缓存 + 负向缓存)。 +- 可取消的查询句柄。 +- 结果直接以 `sockaddr_u` 返回,拿到即可用于 connect。 + +> 第一版**暂不包含**:search domains / ndots、TCP fallback(TC 位截断重查)、nameserver 健康探测、SRV/TXT/MX 等裸记录查询。这些留作后续增强。 + +## 接口 + +```c +#include "hdns.h" + +// 查询地址族 +typedef enum { + HDNS_QUERY_A = 0x01, // 仅 IPv4 + HDNS_QUERY_AAAA = 0x02, // 仅 IPv6 + HDNS_QUERY_BOTH = 0x03, // A + AAAA(默认) +} hdns_family_e; + +// 查询选项(可选;传 NULL 使用默认值) +typedef struct hdns_setting_s { + hdns_family_e family; // 默认 HDNS_QUERY_BOTH + int timeout_ms; // 每次尝试的超时,默认 5000ms + int retries; // 重试次数,默认 2(总尝试次数 = retries + 1) + int use_cache; // 是否使用缓存,默认 1 + const char* nameserver; // 可选,覆盖 nameserver,形如 "8.8.8.8" 或 "8.8.8.8:53";NULL 表示自动 +} hdns_setting_t; + +// 解析结果 +typedef struct hdns_result_s { + int status; // 0 成功;<0 为错误码(HDNS_STATUS_*) + char host[HDNS_NAME_MAXLEN]; // 查询的域名 + int naddrs; // 解析出的地址数量 + sockaddr_u addrs[HDNS_MAX_ADDRS]; // A/AAAA 合并结果(IPv4 在前),端口为 0 +} hdns_result_t; + +// 查询句柄:hdns_t 是一个事件对象(继承自 hevent_t,与 htimer_t / hio_t 同源), +// 因此可携带 event_id、复用 hevent_set_id/hevent_id。 +typedef struct hdns_s hdns_t; + +// 解析完成回调,在 loop 线程被调用。 +// query: 本次查询句柄;回调返回后立即被释放,不要保留。可在此读取其 event_id +// (hevent_id)与调用方的 id 映射关联。result 仅在回调期间有效。 +typedef void (*hdns_cb)(hdns_t* query, const hdns_result_t* result, void* userdata); + +// 发起异步解析,绑定到 loop。返回查询句柄,失败返回 NULL。 +hdns_t* hdns_resolve(hloop_t* loop, const char* host, + hdns_cb cb, void* userdata); + +// 带选项版本 +hdns_t* hdns_resolve_ex(hloop_t* loop, const char* host, + const hdns_setting_t* opt, + hdns_cb cb, void* userdata); + +// 取消进行中的查询并立即释放;返回后回调不会再被调用。 +// 只能在 loop 线程调用,且不得在该查询自己的回调内调用。 +void hdns_cancel(hdns_t* query); + +// 清空该 loop 的 DNS 缓存(如网络切换时)。只能在 loop 线程调用。 +void hdns_clear_cache(hloop_t* loop); +``` + +> C 层句柄的生命周期契约与 `htimer_t` 一致:句柄在查询完成(回调触发)或被 `hdns_cancel` 后失效,之后不得再使用。**如果需要免疫 use-after-free 的句柄,使用 C++ 层 `EventLoop::resolveDns()` / `cancelDns()`**——它维护 `DnsID → hdns_t` 映射(对标 `TimerID`),失效的 `DnsID` 在 `cancelDns` 里查不到即安全 no-op。 + +### 状态码 + +```c +#define HDNS_STATUS_OK 0 // 成功 +#define HDNS_STATUS_TIMEOUT (-1) // 所有尝试均超时 +#define HDNS_STATUS_NXDOMAIN (-2) // 无此域名 / 无地址记录 +#define HDNS_STATUS_SERVFAIL (-3) // 服务器失败 / 响应异常 +#define HDNS_STATUS_BADNAME (-4) // 非法域名 +#define HDNS_STATUS_NONAMESERVER (-5) // 无可用 nameserver +#define HDNS_STATUS_NOMEM (-6) // 内存不足 +#define HDNS_STATUS_CANCELLED (-7) // 已取消(不会回调) +#define HDNS_STATUS_ERROR (-8) // 其它错误 +``` + +## 回调时序保证 + +`hdns_resolve` **绝不会在调用内部同步触发回调**。即使是数字 IP、`/etc/hosts` 命中、缓存命中这类“立即有结果”的情况,完成也会被投递到**下一次 loop 迭代**再回调。因此调用方总能先拿到有效句柄,随后可以确定性地保存或取消它,不会出现回调重入或 use-after-free。 + +## 示例 + +```c +#include "hloop.h" +#include "hdns.h" +#include "hsocket.h" + +static int pending = 0; + +static void on_resolved(hdns_t* query, const hdns_result_t* result, void* userdata) { + hloop_t* loop = (hloop_t*)userdata; + (void)query; + if (result->status == HDNS_STATUS_OK) { + printf("%s =>\n", result->host); + for (int i = 0; i < result->naddrs; ++i) { + char ip[SOCKADDR_STRLEN] = {0}; + sockaddr_ip((sockaddr_u*)&result->addrs[i], ip, sizeof(ip)); + printf(" %s\n", ip); + } + } else { + printf("%s => 解析失败, status=%d\n", result->host, result->status); + } + if (--pending == 0) hloop_stop(loop); +} + +int main() { + hloop_t* loop = hloop_new(0); + const char* hosts[] = { "localhost", "www.example.com", "github.com" }; + pending = 3; + for (int i = 0; i < 3; ++i) { + hdns_resolve(loop, hosts[i], on_resolved, loop); + } + hloop_run(loop); + hloop_free(&loop); + return 0; +} +``` + +完整示例见 `examples/host.c`,性能对比见 `unittest/hdns_benchmark.c`。 + +## C++ 层:EventLoop::resolveDns + +C++ 客户端不直接用 C 层裸指针句柄,而是通过 `EventLoop` 提供的 id 化接口(对标 `setTimer`/`killTimer` 的 `TimerID`): + +```cpp +// 返回免疫 use-after-free 的 DnsID(0 = 失败);cb 在 loop 线程回调 +DnsID resolveDns(const char* host, DnsCallback cb, const hdns_setting_t* opt = NULL); +void cancelDns(DnsID id); // 线程安全;失效 id 自动 no-op +// DnsCallback: void(int status, int naddrs, const sockaddr_u* addrs) +``` + +`EventLoop` 内部维护 `DnsID → hdns_t*` 映射:查询完成时在完成回调里擦除映射项,`cancelDns` 按 id 查表(查不到即安全 no-op)。因此上层只持有一个 `DnsID` 整数,**即使底层 `hdns_t` 已被释放,拿旧 id 去 cancel 也不会 UAF**——与 `TimerID` 完全同构。 + +## 与 connect 路径的集成 + +`hdns` 已接入 C++ 客户端的 connect 路径。**异步解析逻辑统一沉淀在基类 `TcpClientEventLoopTmpl`(`evpp/TcpClient.h`)里**,并通过上面的 `EventLoop::resolveDns` 使用 `DnsID` 句柄,因此所有继承自 `TcpClientTmpl` 的客户端(`TcpClient`、`WebSocketClient`,以及未来任何 `XXXClient`)都**自动获得**异步 DNS,无需各自编写胶水代码。 + +### TcpClientTmpl 派生类(TcpClient / WebSocketClient / ...) + +- 目标是**数字 IP**(或 Unix Domain Socket)时,`createsocket` 走原有同步快速路径立即建 socket,行为不变; +- 目标是**域名**时,`createsocket` 只记录 host/port 并**延迟解析**(不阻塞);`startConnect()`(首次连接与每次重连都会调用)先用 `EventLoop::resolveDns` 异步解析,拿到地址后再在 `startConnectWithAddr()` 建 socket、connect。整个过程**不阻塞事件循环**。旧实现在 loop 线程里调用阻塞的 `getaddrinfo`,会卡住整个 loop。 +- 每次连接/重连都会重新解析,自动应对 DNS 变化; +- 对象只持有 `DnsID`;析构或主动 `closesocket()` 时 `cancelDns` 即可,**无悬垂指针风险**(销毁客户端时查询仍在途也安全,由 `unittest/tcpclient_dns_test` 覆盖)。 +- 解析失败(含首次连接就失败、此时还没有 channel)时,会用一个 NULL-io 的 channel(`isConnected()` 为 false、各方法对空 io 已做防护)走 `onConnection(断开)` 通知,**保证用户总能收到失败回调**(而不是静默丢失);若配置了重连,则继续按重连策略重试。由 `unittest/tcpclient_dns_test` 覆盖。 +- 用户可在 `onConnection` 里用 `channel->error()` 区分失败原因:DNS 解析失败返回 `ERR_DNS_RESOLVE`(见 `herr.h`),连接失败则返回底层 IO 错误(如 `ETIMEDOUT`、连接被拒等)。`Channel` 新增了 `setError()`/`error()`——`error()` 优先返回上层显式设置的错误码,否则回退到 `hio_error(io)`,且对空 io 安全。 + +> 新增客户端零成本:只要继承 `TcpClientTmpl` 并用 `createsocket(port, host)` + `start()`,就自动拥有异步 DNS,不需要实现任何 `onDnsResolved` 之类的回调。 + +### AsyncHttpClient + +`AsyncHttpClient` 不继承 `TcpClientTmpl`(自带连接池等逻辑),单独接入,但同样使用 `EventLoop::resolveDns`: + +- 请求 URL 里是**数字 IP**(或 Unix Domain Socket)时,走原有同步快速路径; +- 请求 URL 里是**域名**时,`doTask` 会先用 `EventLoop::resolveDns` 异步解析,回调里再建立连接、发送请求——同样**不阻塞 loop**。旧实现直接在 loop 线程里对域名做阻塞 `getaddrinfo`。 +- 查询生命周期由 `EventLoop` 的 `DnsID` 映射管理,随 loop 拆除统一回收,无需客户端手工跟踪。 + +> 说明:同步的 `HttpClient` / `requests` 请求路径运行在调用方线程(并非在 loop 内),阻塞解析可接受,故该路径维持不变。 + +## 性能说明 + +`unittest/hdns_benchmark.c` 对比“顺序阻塞 `getaddrinfo`” 与 “单 loop 并发 `hdns`” 解析同一批域名:由于异步解析把所有查询一次性发出、由事件循环并发多路复用,总耗时约等于**一次最慢的解析**,而不是所有解析之和;更关键的是解析期间**事件循环始终不被阻塞**,其它 IO / 定时器可以继续运行。 + +> 注意:单次“命中缓存”的孤立解析,系统 `getaddrinfo`(下游有 `systemd-resolved` 等本地缓存)可能更快;`hdns` 通过尊重 TTL 的进程内缓存来弥补——命中缓存时是进程内查表(微秒级),且同样不阻塞 loop。 diff --git a/event/hdns.c b/event/hdns.c new file mode 100644 index 000000000..4e7d60885 --- /dev/null +++ b/event/hdns.c @@ -0,0 +1,1025 @@ +/* + * Asynchronous DNS resolver — implementation. + * + * A self-contained, non-blocking, native UDP DNS resolver that runs entirely + * inside the hloop event loop. See event/hdns.h for the public API. + * + * This file is intentionally independent from protocol/dns.c (a synchronous + * demo): it carries its own complete DNS wire codec, config loading, query + * engine and TTL cache. + * + * Organization: + * 1. DNS wire codec (build query, parse response, name compression) + * 2. Config (nameservers, hosts) with lazy process-wide cache + * 3. Per-loop resolver (owns UDP io, in-flight table, cache) + * 4. Cache (TTL positive + negative) + * 5. Query lifecycle (hdns_resolve / hdns_cancel / timers / completion) + */ + +#include "hdns.h" +#include "hevent.h" + +#include "hdef.h" +#include "hbase.h" +#include "hlog.h" +#include "htime.h" +#include "hmutex.h" +#include "list.h" + +#ifdef OS_WIN +#include +#ifdef _MSC_VER +#pragma comment(lib, "iphlpapi.lib") +#endif +#endif + +// portable strtok_r (MSVC provides strtok_s with the same signature) +#ifdef _MSC_VER +#define hdns_strtok_r strtok_s +#else +#define hdns_strtok_r strtok_r +#endif + +//============================================================================== +// Constants +//============================================================================== +#define HDNS_TYPE_A 1 +#define HDNS_TYPE_NS 2 +#define HDNS_TYPE_CNAME 5 +#define HDNS_TYPE_AAAA 28 +#define HDNS_CLASS_IN 1 + +#define HDNS_HDR_SIZE 12 +#define HDNS_UDP_BUFSIZE 1500 // enough for a non-EDNS UDP response +#define HDNS_MAX_NAMESERVERS 4 +#define HDNS_MAX_CNAME_HOPS 16 // guard against CNAME loops +#define HDNS_MAX_LABEL_JUMPS 128 // guard against compression-pointer loops + +#define HDNS_CACHE_MIN_TTL 1 // seconds +#define HDNS_CACHE_MAX_TTL 86400 // seconds +#define HDNS_CACHE_NEG_TTL 5 // seconds, negative cache +#define HDNS_CACHE_MAX_ENTRIES 256 + +//============================================================================== +// 1. DNS wire codec +//============================================================================== + +// Encode "www.example.com" into "\3www\7example\3com\0" at dst. +// Returns encoded length (including trailing 0), or -1 on error. +static int hdns__encode_name(const char* name, uint8_t* dst, int dstlen) { + int off = 0; + const char* p = name; + while (*p) { + const char* dot = strchr(p, '.'); + int label_len = dot ? (int)(dot - p) : (int)strlen(p); + if (label_len <= 0 || label_len > 63) return -1; + if (off + 1 + label_len >= dstlen) return -1; + dst[off++] = (uint8_t)label_len; + memcpy(dst + off, p, label_len); + off += label_len; + if (!dot) break; + p = dot + 1; + } + if (off + 1 > dstlen) return -1; + dst[off++] = 0; // root label + return off; +} + +// Decode a (possibly compressed) name starting at buf[*poff]. +// out may be NULL if the caller only wants to advance past the name. +// Returns 0 on success (and updates *poff to just after the name in the +// non-compressed sense), -1 on error. +static int hdns__decode_name(const uint8_t* buf, int buflen, int* poff, + char* out, int outlen) { + int off = *poff; + int outpos = 0; + int jumps = 0; + int advanced_off = -1; // offset to restore after first pointer jump + + while (1) { + if (off < 0 || off >= buflen) return -1; + uint8_t len = buf[off]; + if ((len & 0xC0) == 0xC0) { + // compression pointer + if (off + 1 >= buflen) return -1; + if (advanced_off < 0) advanced_off = off + 2; + int ptr = ((len & 0x3F) << 8) | buf[off + 1]; + off = ptr; + if (++jumps > HDNS_MAX_LABEL_JUMPS) return -1; + continue; + } + if ((len & 0xC0) != 0) return -1; // reserved bits set + if (len == 0) { + off += 1; + break; + } + if (off + 1 + len > buflen) return -1; + if (out) { + if (outpos + len + 1 >= outlen) return -1; + if (outpos > 0) out[outpos++] = '.'; + memcpy(out + outpos, buf + off + 1, len); + outpos += len; + } + off += 1 + len; + } + if (out) out[outpos] = '\0'; + *poff = (advanced_off >= 0) ? advanced_off : off; + return 0; +} + +// Build a DNS query packet for (name, qtype) with the given transaction id. +// Returns packet length, or -1 on error. +static int hdns__build_query(uint16_t txid, const char* name, uint16_t qtype, + uint8_t* buf, int buflen) { + if (buflen < HDNS_HDR_SIZE) return -1; + memset(buf, 0, HDNS_HDR_SIZE); + buf[0] = (uint8_t)(txid >> 8); + buf[1] = (uint8_t)(txid & 0xFF); + buf[2] = 0x01; // RD = 1 (recursion desired) + // qdcount = 1 + buf[4] = 0x00; + buf[5] = 0x01; + + int off = HDNS_HDR_SIZE; + int namelen = hdns__encode_name(name, buf + off, buflen - off); + if (namelen < 0) return -1; + off += namelen; + if (off + 4 > buflen) return -1; + buf[off++] = (uint8_t)(qtype >> 8); + buf[off++] = (uint8_t)(qtype & 0xFF); + buf[off++] = 0x00; // class hi + buf[off++] = HDNS_CLASS_IN; + return off; +} + +// Parse addresses (A/AAAA) out of a DNS response. +// @out_addrs / @max: caller-provided address array. +// @naddrs: number of addresses parsed (appended). +// @min_ttl: filled with the min TTL across accepted address records (seconds). +// Returns: 0 ok (addresses may be 0), HDNS_STATUS_* negative on failure. +static int hdns__parse_response(const uint8_t* buf, int buflen, uint16_t expect_txid, + sockaddr_u* out_addrs, int max, int* naddrs, + uint32_t* min_ttl) { + if (buflen < HDNS_HDR_SIZE) return HDNS_STATUS_SERVFAIL; + uint16_t txid = (buf[0] << 8) | buf[1]; + if (txid != expect_txid) return HDNS_STATUS_SERVFAIL; + // must be a response (QR bit set), not a stray query echo + if (!(buf[2] & 0x80)) return HDNS_STATUS_SERVFAIL; + uint8_t flags2 = buf[3]; + int rcode = flags2 & 0x0F; + if (rcode == 3) return HDNS_STATUS_NXDOMAIN; // NXDOMAIN + if (rcode != 0) return HDNS_STATUS_SERVFAIL; + + int qdcount = (buf[4] << 8) | buf[5]; + int ancount = (buf[6] << 8) | buf[7]; + // NOTE: never trust ancount for allocation; iterate defensively. + + int off = HDNS_HDR_SIZE; + // skip questions + for (int i = 0; i < qdcount; ++i) { + if (hdns__decode_name(buf, buflen, &off, NULL, 0) != 0) return HDNS_STATUS_SERVFAIL; + if (off + 4 > buflen) return HDNS_STATUS_SERVFAIL; + off += 4; // qtype + qclass + } + + uint32_t best_ttl = HDNS_CACHE_MAX_TTL; + int count = *naddrs; + for (int i = 0; i < ancount; ++i) { + if (hdns__decode_name(buf, buflen, &off, NULL, 0) != 0) return HDNS_STATUS_SERVFAIL; + if (off + 10 > buflen) return HDNS_STATUS_SERVFAIL; + uint16_t rtype = (buf[off] << 8) | buf[off + 1]; + // uint16_t rclass = (buf[off+2] << 8) | buf[off+3]; + uint32_t ttl = ((uint32_t)buf[off + 4] << 24) | ((uint32_t)buf[off + 5] << 16) | + ((uint32_t)buf[off + 6] << 8) | (uint32_t)buf[off + 7]; + uint16_t rdlen = (buf[off + 8] << 8) | buf[off + 9]; + off += 10; + if (off + rdlen > buflen) return HDNS_STATUS_SERVFAIL; + + if (rtype == HDNS_TYPE_A && rdlen == 4) { + if (count < max) { + sockaddr_u* a = &out_addrs[count]; + memset(a, 0, sizeof(*a)); + a->sin.sin_family = AF_INET; + memcpy(&a->sin.sin_addr, buf + off, 4); + ++count; + if (ttl < best_ttl) best_ttl = ttl; + } + } else if (rtype == HDNS_TYPE_AAAA && rdlen == 16) { + if (count < max) { + sockaddr_u* a = &out_addrs[count]; + memset(a, 0, sizeof(*a)); + a->sin6.sin6_family = AF_INET6; + memcpy(&a->sin6.sin6_addr, buf + off, 16); + ++count; + if (ttl < best_ttl) best_ttl = ttl; + } + } + // CNAME and other records: skip rdata; the recursive resolver at the + // nameserver has already followed CNAMEs and included A/AAAA in the + // same answer section, so we only need to collect address records. + off += rdlen; + } + + *naddrs = count; + if (min_ttl) *min_ttl = best_ttl; + return HDNS_STATUS_OK; +} + +//============================================================================== +// 2. Config: nameservers + hosts (process-wide, loaded lazily once) +//============================================================================== + +typedef struct hdns_hosts_entry_s { + char name[HDNS_NAME_MAXLEN]; + sockaddr_u addr; + struct list_node node; +} hdns_hosts_entry_t; + +static hmutex_t s_config_mutex; +static honce_t s_config_once = HONCE_INIT; +static int s_config_loaded = 0; +static sockaddr_u s_nameservers[HDNS_MAX_NAMESERVERS]; +static int s_nnameservers = 0; +static struct list_head s_hosts; // list of hdns_hosts_entry_t + +static void hdns__config_init_once(void) { + hmutex_init(&s_config_mutex); + list_init(&s_hosts); +} + +static void hdns__config_lock_init(void) { + // One-time, thread-safe init of the config mutex + hosts list. + honce(&s_config_once, hdns__config_init_once); +} + +static int hdns__parse_ns_ip(const char* ip, sockaddr_u* addr) { + memset(addr, 0, sizeof(*addr)); + + // Accept "ip", "ip:port" (IPv4) and "[ipv6]:port". + char host[INET6_ADDRSTRLEN + 8] = {0}; + int port = HDNS_DEFAULT_PORT; + if (ip[0] == '[') { + // [ipv6]:port + const char* end = strchr(ip, ']'); + if (!end) return -1; + int hlen = (int)(end - ip - 1); + if (hlen <= 0 || hlen >= (int)sizeof(host)) return -1; + memcpy(host, ip + 1, hlen); + host[hlen] = '\0'; + if (end[1] == ':') port = atoi(end + 2); + } else { + const char* colon = strchr(ip, ':'); + // exactly one colon => ipv4:port; more than one => bare IPv6 literal + if (colon && strchr(colon + 1, ':') == NULL) { + int hlen = (int)(colon - ip); + if (hlen <= 0 || hlen >= (int)sizeof(host)) return -1; + memcpy(host, ip, hlen); + host[hlen] = '\0'; + port = atoi(colon + 1); + } else { + strncpy(host, ip, sizeof(host) - 1); + } + } + if (port <= 0 || port > 65535) port = HDNS_DEFAULT_PORT; + + if (inet_pton(AF_INET, host, &addr->sin.sin_addr) == 1) { + addr->sin.sin_family = AF_INET; + addr->sin.sin_port = htons((uint16_t)port); + return 0; + } + if (inet_pton(AF_INET6, host, &addr->sin6.sin6_addr) == 1) { + addr->sin6.sin6_family = AF_INET6; + addr->sin6.sin6_port = htons((uint16_t)port); + return 0; + } + return -1; +} + +static void hdns__add_nameserver(const char* ip) { + if (s_nnameservers >= HDNS_MAX_NAMESERVERS) return; + sockaddr_u addr; + if (hdns__parse_ns_ip(ip, &addr) == 0) { + s_nameservers[s_nnameservers++] = addr; + } +} + +#ifdef OS_WIN +static void hdns__load_nameservers_win(void) { + ULONG buflen = 16 * 1024; + IP_ADAPTER_ADDRESSES* addrs = (IP_ADAPTER_ADDRESSES*)hv_malloc(buflen); + if (!addrs) return; + ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | + GAA_FLAG_SKIP_FRIENDLY_NAME; + ULONG ret = GetAdaptersAddresses(AF_UNSPEC, flags, NULL, addrs, &buflen); + if (ret == ERROR_BUFFER_OVERFLOW) { + hv_free(addrs); + addrs = (IP_ADAPTER_ADDRESSES*)hv_malloc(buflen); + if (!addrs) return; + ret = GetAdaptersAddresses(AF_UNSPEC, flags, NULL, addrs, &buflen); + } + if (ret == NO_ERROR) { + for (IP_ADAPTER_ADDRESSES* ad = addrs; ad; ad = ad->Next) { + if (ad->OperStatus != IfOperStatusUp) continue; + for (IP_ADAPTER_DNS_SERVER_ADDRESS* dns = ad->FirstDnsServerAddress; + dns; dns = dns->Next) { + char ip[INET6_ADDRSTRLEN] = {0}; + struct sockaddr* sa = dns->Address.lpSockaddr; + if (sa->sa_family == AF_INET) { + inet_ntop(AF_INET, &((struct sockaddr_in*)sa)->sin_addr, ip, sizeof(ip)); + } else if (sa->sa_family == AF_INET6) { + inet_ntop(AF_INET6, &((struct sockaddr_in6*)sa)->sin6_addr, ip, sizeof(ip)); + } else { + continue; + } + hdns__add_nameserver(ip); + if (s_nnameservers >= HDNS_MAX_NAMESERVERS) break; + } + if (s_nnameservers >= HDNS_MAX_NAMESERVERS) break; + } + } + hv_free(addrs); +} +#else +static void hdns__load_nameservers_unix(void) { + FILE* fp = fopen("/etc/resolv.conf", "r"); + if (!fp) return; + char line[512]; + while (fgets(line, sizeof(line), fp)) { + char* p = line; + while (*p == ' ' || *p == '\t') ++p; + if (*p == '#' || *p == ';') continue; + if (strncmp(p, "nameserver", 10) != 0) continue; + p += 10; + if (*p != ' ' && *p != '\t') continue; + while (*p == ' ' || *p == '\t') ++p; + // isolate the ip token + char ip[INET6_ADDRSTRLEN + 8] = {0}; + int i = 0; + while (*p && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && + i < (int)sizeof(ip) - 1) { + ip[i++] = *p++; + } + ip[i] = '\0'; + hdns__add_nameserver(ip); + if (s_nnameservers >= HDNS_MAX_NAMESERVERS) break; + } + fclose(fp); +} +#endif + +static void hdns__load_hosts(void) { + const char* path; +#ifdef OS_WIN + char winpath[MAX_PATH] = {0}; + const char* sysroot = getenv("SystemRoot"); + if (!sysroot) sysroot = "C:\\Windows"; + snprintf(winpath, sizeof(winpath), "%s\\System32\\drivers\\etc\\hosts", sysroot); + path = winpath; +#else + path = "/etc/hosts"; +#endif + FILE* fp = fopen(path, "r"); + if (!fp) return; + char line[1024]; + while (fgets(line, sizeof(line), fp)) { + // strip comment + char* hash = strchr(line, '#'); + if (hash) *hash = '\0'; + // first token = ip, remaining tokens = names + char* save = NULL; + char* ip = hdns_strtok_r(line, " \t\r\n", &save); + if (!ip) continue; + sockaddr_u addr; + if (hdns__parse_ns_ip(ip, &addr) != 0) continue; // reuse: parses ip, sets port 53 (ignored) + char* name; + while ((name = hdns_strtok_r(NULL, " \t\r\n", &save)) != NULL) { + hdns_hosts_entry_t* e; + HV_ALLOC_SIZEOF(e); + if (!e) continue; + strncpy(e->name, name, sizeof(e->name) - 1); + e->addr = addr; + e->addr.sin.sin_port = 0; // hosts entries carry no port + list_add(&e->node, &s_hosts); + } + } + fclose(fp); +} + +static void hdns__load_config_locked(void) { + if (s_config_loaded) return; + s_config_loaded = 1; + s_nnameservers = 0; +#ifdef OS_WIN + hdns__load_nameservers_win(); +#else + hdns__load_nameservers_unix(); +#endif + if (s_nnameservers == 0) { + // universal fallback + hdns__add_nameserver(HDNS_FALLBACK_NAMESERVER); + } + hdns__load_hosts(); +} + +// Look up @name in /etc/hosts. Returns number of matched addresses appended. +static int hdns__hosts_lookup(const char* name, hdns_family_e family, + sockaddr_u* out, int max) { + int count = 0; + struct list_node* node; + list_for_each(node, &s_hosts) { + hdns_hosts_entry_t* e = list_entry(node, hdns_hosts_entry_t, node); + if (strcasecmp(e->name, name) != 0) continue; + int fam = e->addr.sa.sa_family; + if (fam == AF_INET && !(family & HDNS_QUERY_A)) continue; + if (fam == AF_INET6 && !(family & HDNS_QUERY_AAAA)) continue; + if (count < max) out[count++] = e->addr; + } + return count; +} + +//============================================================================== +// 3/4. Per-loop resolver + cache +//============================================================================== + +typedef struct hdns_cache_entry_s { + char host[HDNS_NAME_MAXLEN]; + int family; // hdns_family_e bitset + int naddrs; + sockaddr_u addrs[HDNS_MAX_ADDRS]; + uint64_t expire_ms; // loop clock ms + int negative; // 1 = cached failure + struct list_node node; +} hdns_cache_entry_t; + +// One logical resolve. +// NOTE: hdns_s is a subclass of hevent_t (like htimer_s / hio_s): it starts +// with HEVENT_FIELDS so it inherits `loop`, `event_id`, `userdata`, `priority` +// and the destroy/active/pending flags, and can use hevent_set_id/hevent_id. +// It is NOT scheduled by the loop as an event, though; it is driven by the +// resolver's UDP io + timers. HEVENT_FIELDS is reused only for a uniform +// layout and a built-in id slot. +struct hdns_s { + HEVENT_FIELDS // loop, event_type, event_id, cb, userdata, priority, flags... + // extra 1-bit flags packed right after HEVENT_FLAGS (destroy/active/pending) + // to share the same storage unit and save space. + unsigned detached : 1; // cancelled: run to completion, drop result + unsigned delivering : 1; // inside hdns__deliver's callback (cancel guard) + struct hdns_resolver_s* resolver; + char host[HDNS_NAME_MAXLEN]; + hdns_setting_t opt; + hdns_cb dns_cb; // user callback (hevent_t::cb has a different type) + + // sub-queries: index 0 = A, index 1 = AAAA (per opt.family) + struct { + int active; // waiting for a response + int done; // settled (answer/empty/error) + uint16_t txid; + uint16_t qtype; + } sub[2]; + + int attempt; // current attempt (0..retries) + int ns_index; // current nameserver index + sockaddr_u last_ns; // nameserver the current attempt was sent to + htimer_t* timer; // per-attempt timeout timer + htimer_t* defer_timer; // for async delivery of immediate results + + // accumulated results + sockaddr_u addrs[HDNS_MAX_ADDRS]; + int naddrs; + uint32_t min_ttl; + int any_error; // last non-timeout error seen + + struct list_node node; // in resolver->queries +}; + +typedef struct hdns_resolver_s { + hloop_t* loop; + hio_t* io4; // UDP socket for IPv4 nameservers (lazy) + hio_t* io6; // UDP socket for IPv6 nameservers (lazy) + struct list_head queries; // in-flight hdns_t + struct list_head cache; // hdns_cache_entry_t (LRU-ish by insertion) + int ncache; + uint16_t next_txid; // monotonic base for sub-query txids (wire) + uint8_t sndbuf[HDNS_UDP_BUFSIZE]; +} hdns_resolver_t; + +// forward decls +static void hdns__on_udp_read(hio_t* io, void* buf, int readbytes); +static void hdns__on_timeout(htimer_t* timer); +static void hdns__on_defer(htimer_t* timer); +static void hdns__send_queries(hdns_t* q); +static void hdns__finish(hdns_t* q, int status); + +// Lazily create the UDP send/recv socket matching the nameserver's family. +// A socket's family must match sendto()'s destination, so IPv4 and IPv6 +// nameservers need separate sockets. Returns NULL on failure. +static hio_t* hdns__resolver_io(hdns_resolver_t* r, int family) { + hio_t** pio = (family == AF_INET6) ? &r->io6 : &r->io4; + if (*pio) return *pio; + int fd = socket(family, SOCK_DGRAM, 0); + if (fd < 0) return NULL; + hio_t* io = hio_get(r->loop, fd); + if (!io) { + closesocket(fd); + return NULL; + } + hio_set_type(io, HIO_TYPE_UDP); + hio_setcb_read(io, hdns__on_udp_read); + hio_set_context(io, r); + hio_read(io); + *pio = io; + return io; +} + +static hdns_resolver_t* hdns__resolver_get(hloop_t* loop) { + hdns_resolver_t* r = (hdns_resolver_t*)loop->dns_resolver; + if (r) return r; + + HV_ALLOC_SIZEOF(r); + if (!r) return NULL; + r->loop = loop; + list_init(&r->queries); + list_init(&r->cache); + r->ncache = 0; + // seed the txid counter randomly to reduce off-path spoofing predictability. + r->next_txid = (uint16_t)hv_rand(1, 0xFFFF); + // sockets are created lazily per nameserver family in hdns__resolver_io(). + + loop->dns_resolver = r; + return r; +} + +void hdns_resolver_free(hloop_t* loop) { + hdns_resolver_t* r = (hdns_resolver_t*)loop->dns_resolver; + if (!r) return; + loop->dns_resolver = NULL; + + // cancel outstanding queries (no callbacks on teardown) + struct list_node *node, *tmp; + list_for_each_safe(node, tmp, &r->queries) { + hdns_t* q = list_entry(node, hdns_t, node); + list_del(&q->node); + if (q->timer) htimer_del(q->timer); + if (q->defer_timer) htimer_del(q->defer_timer); + HV_FREE(q); + } + // free cache + list_for_each_safe(node, tmp, &r->cache) { + hdns_cache_entry_t* e = list_entry(node, hdns_cache_entry_t, node); + list_del(&e->node); + HV_FREE(e); + } + // NOTE: r->io4/io6 are owned by the loop and freed by hloop_cleanup's io sweep. + HV_FREE(r); +} + +//---------------------------- cache ------------------------------------------ + +static hdns_cache_entry_t* hdns__cache_find(hdns_resolver_t* r, const char* host, + int family) { + uint64_t now = hloop_now_ms(r->loop); + struct list_node *node, *tmp; + list_for_each_safe(node, tmp, &r->cache) { + hdns_cache_entry_t* e = list_entry(node, hdns_cache_entry_t, node); + if (now >= e->expire_ms) { + // expired -> evict + list_del(&e->node); + HV_FREE(e); + --r->ncache; + continue; + } + if (e->family == family && strcasecmp(e->host, host) == 0) { + return e; + } + } + return NULL; +} + +static void hdns__cache_put(hdns_resolver_t* r, const char* host, int family, + const sockaddr_u* addrs, int naddrs, + uint32_t ttl_sec, int negative) { + // drop existing entry for same key + struct list_node *node, *tmp; + list_for_each_safe(node, tmp, &r->cache) { + hdns_cache_entry_t* e = list_entry(node, hdns_cache_entry_t, node); + if (e->family == family && strcasecmp(e->host, host) == 0) { + list_del(&e->node); + HV_FREE(e); + --r->ncache; + break; + } + } + // bound cache size: evict oldest (list tail) when full + if (r->ncache >= HDNS_CACHE_MAX_ENTRIES && !list_empty(&r->cache)) { + hdns_cache_entry_t* old = list_entry(r->cache.prev, hdns_cache_entry_t, node); + list_del(&old->node); + HV_FREE(old); + --r->ncache; + } + + hdns_cache_entry_t* e; + HV_ALLOC_SIZEOF(e); + if (!e) return; + strncpy(e->host, host, sizeof(e->host) - 1); + e->family = family; + e->negative = negative; + e->naddrs = 0; + if (!negative && addrs && naddrs > 0) { + e->naddrs = naddrs > HDNS_MAX_ADDRS ? HDNS_MAX_ADDRS : naddrs; + memcpy(e->addrs, addrs, e->naddrs * sizeof(sockaddr_u)); + } + if (ttl_sec < HDNS_CACHE_MIN_TTL) ttl_sec = HDNS_CACHE_MIN_TTL; + if (ttl_sec > HDNS_CACHE_MAX_TTL) ttl_sec = HDNS_CACHE_MAX_TTL; + e->expire_ms = hloop_now_ms(r->loop) + (uint64_t)ttl_sec * 1000; + list_add(&e->node, &r->cache); // newest at head + ++r->ncache; +} + +void hdns_clear_cache(hloop_t* loop) { + hdns_resolver_t* r = (hdns_resolver_t*)loop->dns_resolver; + if (!r) return; + struct list_node *node, *tmp; + list_for_each_safe(node, tmp, &r->cache) { + hdns_cache_entry_t* e = list_entry(node, hdns_cache_entry_t, node); + list_del(&e->node); + HV_FREE(e); + } + r->ncache = 0; +} + +//============================================================================== +// 5. Query lifecycle +//============================================================================== + +// Fill an hdns_result_t and invoke the user callback, then free the query. +// A cancelled (detached) query is freed silently without invoking any callback. +static void hdns__deliver(hdns_t* q, int status) { + hdns_cb cb = q->detached ? NULL : q->dns_cb; + void* ud = q->userdata; + + hdns_result_t result; + if (cb) { + memset(&result, 0, sizeof(result)); + result.status = status; + strncpy(result.host, q->host, sizeof(result.host) - 1); + if (status == HDNS_STATUS_OK) { + result.naddrs = q->naddrs > HDNS_MAX_ADDRS ? HDNS_MAX_ADDRS : q->naddrs; + memcpy(result.addrs, q->addrs, result.naddrs * sizeof(sockaddr_u)); + } + } + + // Detach from the resolver and stop timers, then invoke the callback while + // the handle is still alive (the cb receives it, e.g. to read hevent_id), + // and finally free it. + // NOTE: the public API forbids calling hdns_cancel(q) from within q's own + // callback. hdns_cancel() has a re-entrancy guard (see below) so such misuse + // fails safe rather than double-freeing, but callers must not rely on it. + if (q->node.next) { list_del(&q->node); q->node.next = q->node.prev = NULL; } + if (q->timer) { htimer_del(q->timer); q->timer = NULL; } + if (q->defer_timer) { htimer_del(q->defer_timer); q->defer_timer = NULL; } + + q->delivering = 1; + if (cb) cb(q, &result, ud); + + HV_FREE(q); +} + +// Sort accumulated addresses: IPv4 first (stable), matching getaddrinfo-ish order. +static void hdns__sort_v4_first(hdns_t* q) { + sockaddr_u tmp[HDNS_MAX_ADDRS]; + int n = 0; + for (int i = 0; i < q->naddrs; ++i) { + if (q->addrs[i].sa.sa_family == AF_INET) tmp[n++] = q->addrs[i]; + } + for (int i = 0; i < q->naddrs; ++i) { + if (q->addrs[i].sa.sa_family != AF_INET) tmp[n++] = q->addrs[i]; + } + memcpy(q->addrs, tmp, n * sizeof(sockaddr_u)); + q->naddrs = n; +} + +// Called when all requested sub-queries have settled. +static void hdns__complete(hdns_t* q) { + hdns_resolver_t* r = q->resolver; + int status; + if (q->naddrs > 0) { + hdns__sort_v4_first(q); + status = HDNS_STATUS_OK; + if (q->opt.use_cache) { + hdns__cache_put(r, q->host, q->opt.family, q->addrs, q->naddrs, + q->min_ttl, 0); + } + } else { + status = q->any_error ? q->any_error : HDNS_STATUS_NXDOMAIN; + if (q->opt.use_cache && status == HDNS_STATUS_NXDOMAIN) { + hdns__cache_put(r, q->host, q->opt.family, NULL, 0, + HDNS_CACHE_NEG_TTL, 1); + } + } + hdns__deliver(q, status); +} + +static int hdns__all_done(hdns_t* q) { + for (int i = 0; i < 2; ++i) { + if (q->sub[i].qtype && !q->sub[i].done) return 0; + } + return 1; +} + +// Send (or resend) all not-yet-done sub-queries to the current nameserver. +static void hdns__send_queries(hdns_t* q) { + hdns_resolver_t* r = q->resolver; + hdns__config_lock_init(); + hmutex_lock(&s_config_mutex); + hdns__load_config_locked(); + + sockaddr_u ns; + if (q->opt.nameserver) { + if (hdns__parse_ns_ip(q->opt.nameserver, &ns) != 0) { + hmutex_unlock(&s_config_mutex); + hdns__finish(q, HDNS_STATUS_NONAMESERVER); + return; + } + } else { + if (s_nnameservers == 0) { + hmutex_unlock(&s_config_mutex); + hdns__finish(q, HDNS_STATUS_NONAMESERVER); + return; + } + ns = s_nameservers[q->ns_index % s_nnameservers]; + } + hmutex_unlock(&s_config_mutex); + + // pick the UDP socket matching the nameserver's address family. + hio_t* io = hdns__resolver_io(r, ns.sa.sa_family); + if (io == NULL) { + hdns__finish(q, HDNS_STATUS_NONAMESERVER); + return; + } + // remember which nameserver this attempt targets, to validate responses. + q->last_ns = ns; + + for (int i = 0; i < 2; ++i) { + if (!q->sub[i].qtype || q->sub[i].done) continue; + int len = hdns__build_query(q->sub[i].txid, q->host, q->sub[i].qtype, + r->sndbuf, sizeof(r->sndbuf)); + if (len < 0) { + q->sub[i].done = 1; + q->any_error = HDNS_STATUS_BADNAME; + continue; + } + q->sub[i].active = 1; + hio_sendto(io, r->sndbuf, len, &ns.sa); + } + + if (hdns__all_done(q)) { + // build failures for all sub-queries + hdns__complete(q); + return; + } + + // (re)arm per-attempt timeout + if (q->timer) { htimer_del(q->timer); q->timer = NULL; } + q->timer = htimer_add(r->loop, hdns__on_timeout, q->opt.timeout_ms, 1); + if (q->timer) hevent_set_userdata(q->timer, q); +} + +// Per-attempt timeout: retry (rotate nameserver) or give up. +static void hdns__on_timeout(htimer_t* timer) { + hdns_t* q = (hdns_t*)hevent_userdata(timer); + if (!q) return; + q->timer = NULL; // this single-shot timer auto-deletes after firing + + if (q->attempt < q->opt.retries) { + ++q->attempt; + ++q->ns_index; // rotate to next nameserver + hdns__send_queries(q); + } else { + // mark unfinished sub-queries as timed out + for (int i = 0; i < 2; ++i) { + if (q->sub[i].qtype && !q->sub[i].done) { + q->sub[i].done = 1; + q->sub[i].active = 0; + if (!q->any_error) q->any_error = HDNS_STATUS_TIMEOUT; + } + } + hdns__complete(q); + } +} + +// Terminal helper for setup errors (async-safe: schedules delivery). +static void hdns__finish(hdns_t* q, int status) { + q->naddrs = 0; + q->any_error = status; + hdns__deliver(q, status); +} + +// UDP read: demux by transaction id to the matching sub-query. +static void hdns__on_udp_read(hio_t* io, void* buf, int readbytes) { + hdns_resolver_t* r = (hdns_resolver_t*)hio_context(io); + if (!r || readbytes < HDNS_HDR_SIZE) return; + const uint8_t* pkt = (const uint8_t*)buf; + uint16_t txid = (pkt[0] << 8) | pkt[1]; + // source of this datagram, to guard against off-path spoofed responses. + struct sockaddr* src = hio_peeraddr(io); + + // find the query + sub-query for this txid + struct list_node* node; + list_for_each(node, &r->queries) { + hdns_t* q = list_entry(node, hdns_t, node); + for (int i = 0; i < 2; ++i) { + if (!q->sub[i].qtype || q->sub[i].done) continue; + if (q->sub[i].txid != txid) continue; + + // Only accept a response coming from the nameserver we queried. + // This drops off-path injected packets that happen to guess the + // txid but not the source address/port. Skip this match and keep + // scanning (don't abort the whole sweep) so a legitimate response + // for another query in the same datagram batch is still handled. + if (src && sockaddr_compare((sockaddr_u*)src, &q->last_ns) != 0) { + continue; + } + + // parse this sub-query's answer + uint32_t ttl = HDNS_CACHE_MAX_TTL; + int rc = hdns__parse_response(pkt, readbytes, txid, + q->addrs, HDNS_MAX_ADDRS, + &q->naddrs, &ttl); + if (rc == HDNS_STATUS_OK) { + if (ttl < q->min_ttl) q->min_ttl = ttl; + } else if (rc == HDNS_STATUS_NXDOMAIN) { + if (!q->any_error) q->any_error = HDNS_STATUS_NXDOMAIN; + } else { + if (!q->any_error) q->any_error = HDNS_STATUS_SERVFAIL; + } + q->sub[i].done = 1; + q->sub[i].active = 0; + + if (hdns__all_done(q)) { + hdns__complete(q); + } + return; + } + } + // unknown txid: stale/duplicate response, ignore. +} + +// Deferred delivery for immediate results (numeric IP / hosts / cache hit). +static void hdns__on_defer(htimer_t* timer) { + hdns_t* q = (hdns_t*)hevent_userdata(timer); + if (!q) return; + q->defer_timer = NULL; // single-shot auto-deletes + hdns__complete(q); +} + +// Deferred start of the network query, so hdns_resolve() never sends (or +// completes, or invokes the callback) re-entrantly inside the call. +static void hdns__on_start(htimer_t* timer) { + hdns_t* q = (hdns_t*)hevent_userdata(timer); + if (!q) return; + q->defer_timer = NULL; // single-shot auto-deletes + hdns__send_queries(q); +} + +static void hdns__defer_complete(hdns_t* q) { + // Schedule completion on the next loop tick so cb never runs re-entrantly + // inside hdns_resolve() (see the "NEVER invoked re-entrantly" guarantee in + // hdns.h). htimer_add() with timeout_ms=1 does not return NULL, so this is + // always deferred; on the (unreachable) alloc-failure path we leave the + // query registered to be cleaned up at loop teardown rather than completing + // synchronously and breaking the invariant. + q->defer_timer = htimer_add(q->loop, hdns__on_defer, 1, 1); + if (q->defer_timer) { + hevent_set_userdata(q->defer_timer, q); + } +} + +hdns_t* hdns_resolve_ex(hloop_t* loop, const char* host, + const hdns_setting_t* opt, + hdns_cb cb, void* userdata) { + if (!loop || !host || !cb) return NULL; + size_t hlen = strlen(host); + if (hlen == 0 || hlen >= HDNS_NAME_MAXLEN) return NULL; + + hdns_resolver_t* r = hdns__resolver_get(loop); + if (!r) return NULL; + + hdns_t* q; + HV_ALLOC_SIZEOF(q); + if (!q) return NULL; + q->loop = loop; + q->event_type = HEVENT_TYPE_CUSTOM; + q->resolver = r; + strncpy(q->host, host, sizeof(q->host) - 1); + q->dns_cb = cb; + q->userdata = userdata; + q->min_ttl = HDNS_CACHE_MAX_TTL; + + // options with defaults + if (opt) { + q->opt = *opt; + } else { + q->opt.family = HDNS_QUERY_BOTH; + q->opt.timeout_ms = HDNS_DEFAULT_TIMEOUT_MS; + q->opt.retries = HDNS_DEFAULT_RETRIES; + q->opt.use_cache = 1; + q->opt.nameserver = NULL; + } + if (q->opt.family == 0) q->opt.family = HDNS_QUERY_BOTH; + if (q->opt.timeout_ms <= 0) q->opt.timeout_ms = HDNS_DEFAULT_TIMEOUT_MS; + if (q->opt.retries < 0) q->opt.retries = 0; + + // register in resolver so it can be cancelled / matched + list_add(&q->node, &r->queries); + + // NOTE: htimer_add(loop, cb, 1, 1) below never returns NULL (timeout_ms>0), + // so completion is always deferred to the next loop tick and this function + // always returns a still-valid handle before any callback runs. + + // 1) numeric IP fast path + sockaddr_u num; + memset(&num, 0, sizeof(num)); + if (inet_pton(AF_INET, host, &num.sin.sin_addr) == 1) { + num.sin.sin_family = AF_INET; + q->addrs[q->naddrs++] = num; + hdns__defer_complete(q); + return q; + } + if (inet_pton(AF_INET6, host, &num.sin6.sin6_addr) == 1) { + num.sin6.sin6_family = AF_INET6; + q->addrs[q->naddrs++] = num; + hdns__defer_complete(q); + return q; + } + + // 2) hosts + cache lookups (need config loaded) + hdns__config_lock_init(); + hmutex_lock(&s_config_mutex); + hdns__load_config_locked(); + int nhosts = hdns__hosts_lookup(host, q->opt.family, q->addrs, HDNS_MAX_ADDRS); + hmutex_unlock(&s_config_mutex); + if (nhosts > 0) { + q->naddrs = nhosts; + hdns__defer_complete(q); + return q; + } + + if (q->opt.use_cache) { + hdns_cache_entry_t* ce = hdns__cache_find(r, host, q->opt.family); + if (ce) { + if (ce->negative) { + q->any_error = HDNS_STATUS_NXDOMAIN; + } else { + q->naddrs = ce->naddrs; + memcpy(q->addrs, ce->addrs, ce->naddrs * sizeof(sockaddr_u)); + } + hdns__defer_complete(q); + return q; + } + } + + // 3) network query: assign sub-queries + txids, and start on the next loop + // tick so this function returns the handle before any send/callback. + // A per-resolver monotonic 16-bit counter (base, base+1 for A/AAAA) is + // used for demuxing responses. NOTE: after 65536 it wraps and could in + // theory collide with a still-in-flight query, but that would require + // tens of thousands of resolves on one loop with an old query still + // pending -- it does not happen in practice, so we keep this simple. + uint16_t base = r->next_txid; + r->next_txid += 2; // A uses base, AAAA uses base+1 + if (q->opt.family & HDNS_QUERY_A) { + q->sub[0].qtype = HDNS_TYPE_A; + q->sub[0].txid = base; + } + if (q->opt.family & HDNS_QUERY_AAAA) { + q->sub[1].qtype = HDNS_TYPE_AAAA; + q->sub[1].txid = (uint16_t)(base + 1); + } + // Start the network query on the next loop tick so this function returns + // the handle before anything is sent (see the re-entrancy guarantee). + // htimer_add(timeout_ms=1) does not return NULL; on the unreachable + // alloc-failure path the query stays registered and is cleaned at teardown. + q->defer_timer = htimer_add(loop, hdns__on_start, 1, 1); + if (q->defer_timer) { + hevent_set_userdata(q->defer_timer, q); + } + return q; +} + +hdns_t* hdns_resolve(hloop_t* loop, const char* host, + hdns_cb cb, void* userdata) { + return hdns_resolve_ex(loop, host, NULL, cb, userdata); +} + +void hdns_cancel(hdns_t* q) { + if (!q) return; + // Fail-safe against the forbidden re-entrant case: if q is currently being + // delivered (cancel called from within its own callback), hdns__deliver + // will free it right after the callback returns, so do nothing here to + // avoid a double-free. + if (q->delivering) return; + if (q->node.next) list_del(&q->node); + if (q->timer) { htimer_del(q->timer); q->timer = NULL; } + if (q->defer_timer) { htimer_del(q->defer_timer); q->defer_timer = NULL; } + HV_FREE(q); +} diff --git a/event/hdns.h b/event/hdns.h new file mode 100644 index 000000000..8afaf0166 --- /dev/null +++ b/event/hdns.h @@ -0,0 +1,138 @@ +#ifndef HV_DNS_ASYNC_H_ +#define HV_DNS_ASYNC_H_ + +/* + * Asynchronous DNS resolver. + * + * A self-contained, non-blocking, native UDP DNS resolver that runs entirely + * inside the hloop event loop. Unlike the blocking getaddrinfo() used by + * ResolveAddr(), hdns_resolve() never stalls the event loop: DNS queries are + * sent over a non-blocking UDP socket and results are delivered through a + * callback in the loop thread. + * + * NOTE: This is independent from protocol/dns.h (which is a synchronous demo). + * + * Features: + * - Async A (IPv4) / AAAA (IPv6) queries. + * - Reads nameservers from /etc/resolv.conf (Unix) or GetAdaptersAddresses (Windows), + * falling back to 8.8.8.8. + * - Loads /etc/hosts and answers from it before querying the network. + * - Numeric-IP fast path (no query for literal IPv4/IPv6). + * - Per-query timeout, retry and multi-nameserver rotation. + * - DNS name compression parsing. Collects the A/AAAA address records from + * the answer section; CNAMEs are not chased client-side, relying on the + * recursive nameserver to include the final A/AAAA records (as recursive + * resolvers do). Non-recursive / CNAME-only responses are not followed. + * - TTL-respecting per-loop cache (positive + negative). + * - Cancelable query handle. + * + * @see examples/host.c + */ + +#include "hexport.h" +#include "hplatform.h" +#include "hsocket.h" // sockaddr_u +#include "hloop.h" // hloop_t + +#define HDNS_DEFAULT_PORT 53 +#define HDNS_DEFAULT_TIMEOUT_MS 5000 +#define HDNS_DEFAULT_RETRIES 2 // total attempts = retries + 1 +#define HDNS_FALLBACK_NAMESERVER "8.8.8.8" +#define HDNS_MAX_ADDRS 16 +#define HDNS_NAME_MAXLEN 256 + +// hdns_result_t.status codes (0 = success, negative = failure) +#define HDNS_STATUS_OK 0 +#define HDNS_STATUS_TIMEOUT (-1) // all attempts timed out +#define HDNS_STATUS_NXDOMAIN (-2) // no such domain / no address record +#define HDNS_STATUS_SERVFAIL (-3) // server failure / bad response +#define HDNS_STATUS_BADNAME (-4) // invalid host name +#define HDNS_STATUS_NONAMESERVER (-5) // no usable nameserver +#define HDNS_STATUS_NOMEM (-6) // out of memory +#define HDNS_STATUS_CANCELLED (-7) // query cancelled (not delivered to cb) +#define HDNS_STATUS_ERROR (-8) // other error + +typedef enum { + HDNS_QUERY_A = 0x01, // IPv4 only + HDNS_QUERY_AAAA = 0x02, // IPv6 only + HDNS_QUERY_BOTH = 0x03, // A + AAAA (default) +} hdns_family_e; + +typedef struct hdns_setting_s { + hdns_family_e family; // default HDNS_QUERY_BOTH + int timeout_ms; // per-attempt timeout, default HDNS_DEFAULT_TIMEOUT_MS + int retries; // default HDNS_DEFAULT_RETRIES + int use_cache; // default 1 + const char* nameserver; // optional override "ip" or "ip:port", NULL = auto + +#ifdef __cplusplus + hdns_setting_s() { + family = HDNS_QUERY_BOTH; + timeout_ms = HDNS_DEFAULT_TIMEOUT_MS; + retries = HDNS_DEFAULT_RETRIES; + use_cache = 1; + nameserver = NULL; + } +#endif +} hdns_setting_t; + +typedef struct hdns_result_s { + int status; // 0:ok <0:herr code + char host[HDNS_NAME_MAXLEN]; // queried name + int naddrs; // number of resolved addresses + sockaddr_u addrs[HDNS_MAX_ADDRS]; // A/AAAA merged (IPv4 first), port = 0 +} hdns_result_t; + +// Cancelable query handle. +// +// hdns_t is an event object (subclass of hevent_t, like htimer_t / hio_t), so +// it can carry an event_id and integrate with the loop's conventions. +// +// C API lifetime contract (same as htimer_t): the handle is valid until the +// query completes (its callback fires) or it is cancelled via hdns_cancel(). +// Do NOT keep or use the pointer after that. For a use-after-free-proof handle, +// use the C++ EventLoop::resolveDns()/cancelDns() which manage an id -> hdns_t +// map (mirrors TimerID), just like htimer_t vs EventLoop TimerID. +typedef struct hdns_s hdns_t; + +/* + * result callback, invoked in the loop thread. + * @query: the handle returned by hdns_resolve*(); it is freed immediately + * after this callback returns, so do NOT retain it. Its event_id + * (hevent_id) can be read here to correlate with a caller-side map. + * @result: only valid during the callback. + */ +typedef void (*hdns_cb)(hdns_t* query, const hdns_result_t* result, void* userdata); + +BEGIN_EXTERN_C + +/* + * Start an asynchronous resolve bound to @loop. @cb runs in the loop thread. + * + * The callback is NEVER invoked re-entrantly inside this call: even for + * numeric-IP / hosts / cache hits, completion is posted to the next loop + * iteration. Therefore the returned handle is always valid to pass to + * hdns_cancel() until the callback fires. + * + * @return a query handle, or NULL on immediate failure (invalid params / OOM). + */ +HV_EXPORT hdns_t* hdns_resolve(hloop_t* loop, const char* host, + hdns_cb cb, void* userdata); + +HV_EXPORT hdns_t* hdns_resolve_ex(hloop_t* loop, const char* host, + const hdns_setting_t* opt, + hdns_cb cb, void* userdata); + +/* + * Cancel an in-flight query and free it immediately. After this returns, @cb + * will NOT be called and @query is invalid. MUST be called from the loop + * thread, and MUST NOT be called from within that query's own callback. + */ +HV_EXPORT void hdns_cancel(hdns_t* query); + +// Clear the per-loop DNS cache (e.g. on a network change). Loop-thread only. +HV_EXPORT void hdns_clear_cache(hloop_t* loop); + +END_EXTERN_C + +#endif // HV_DNS_ASYNC_H_ diff --git a/event/hevent.h b/event/hevent.h index 1650b22a5..8f72a0317 100644 --- a/event/hevent.h +++ b/event/hevent.h @@ -66,10 +66,15 @@ struct hloop_s { int eventfds[2]; event_queue custom_events; hmutex_t custom_events_mutex; + // async dns resolver (event/hdns.c), created lazily, freed in hloop_cleanup + void* dns_resolver; }; uint64_t hloop_next_event_id(); +// async dns resolver (event/hdns.c): free per-loop resolver, called in hloop_cleanup. +void hdns_resolver_free(hloop_t* loop); + struct hidle_s { HEVENT_FIELDS uint32_t repeat; diff --git a/event/hloop.c b/event/hloop.c index 6b1fcab83..27ec8acef 100644 --- a/event/hloop.c +++ b/event/hloop.c @@ -360,6 +360,10 @@ static void hloop_cleanup(hloop_t* loop) { loop->pendings[i] = NULL; } + // async dns resolver + printd("cleanup dns_resolver...\n"); + hdns_resolver_free(loop); + // ios printd("cleanup ios...\n"); for (int i = 0; i < loop->ios.maxsize; ++i) { diff --git a/evpp/Channel.h b/evpp/Channel.h index 55e078bfb..6751c6fe5 100644 --- a/evpp/Channel.h +++ b/evpp/Channel.h @@ -22,6 +22,7 @@ class Channel { fd_ = -1; id_ = 0; ctx_ = NULL; + error_ = 0; status = CLOSED; if (io) { fd_ = hio_fd(io); @@ -56,7 +57,13 @@ class Channel { hio_t* io() { return io_; } int fd() { return fd_; } uint32_t id() { return id_; } - int error() { return hio_error(io_); } + // Returns the failure reason. An explicitly-set error (setError, e.g. a DNS + // resolve failure before any io exists) takes precedence; otherwise the + // io-layer error is returned. Null-safe: a NULL-io channel returns error_. + int error() { return error_ ? error_ : (io_ ? hio_error(io_) : 0); } + // Set an upper-layer error reason (e.g. ERR_DNS_RESOLVE) to be surfaced via + // error(). Useful when there is no io_ to carry the error. + void setError(int err) { error_ = err; } // context void* context() { @@ -194,6 +201,7 @@ class Channel { int fd_; uint32_t id_; void* ctx_; + int error_; // upper-layer error (e.g. ERR_DNS_RESOLVE); see error() enum Status { OPENED, CONNECTING, diff --git a/evpp/Event.h b/evpp/Event.h index 44d0c7e1f..a60d7f80d 100644 --- a/evpp/Event.h +++ b/evpp/Event.h @@ -5,17 +5,28 @@ #include #include "hloop.h" +#include "hdns.h" +#include "hsocket.h" namespace hv { struct Event; struct Timer; +struct DnsQuery; typedef uint64_t TimerID; #define INVALID_TIMER_ID ((hv::TimerID)-1) +// DNS query id: a use-after-free-proof handle for EventLoop::resolveDns(). +// Mirrors TimerID: the EventLoop keeps a DnsID -> DnsQuery map, so a stale id +// (completed / cancelled query) simply misses the map and cancelDns() no-ops. +typedef uint64_t DnsID; +#define INVALID_DNS_ID ((hv::DnsID)0) + typedef std::function EventCallback; typedef std::function TimerCallback; +// DNS resolve result: status==0 on success; addrs are A/AAAA (IPv4 first). +typedef std::function DnsCallback; struct Event { hevent_t event; @@ -39,8 +50,22 @@ struct Timer { } }; +// Tracks one in-flight EventLoop::resolveDns() query. The EventLoop owns the +// DnsID -> DnsQuery map; the underlying hdns_t* is owned by the C resolver and +// carries this DnsID in its event_id so completion can erase the map entry. +struct DnsQuery { + hdns_t* query; + DnsCallback cb; + + DnsQuery(hdns_t* query = NULL, DnsCallback cb = NULL) { + this->query = query; + this->cb = std::move(cb); + } +}; + typedef std::shared_ptr EventPtr; typedef std::shared_ptr TimerPtr; +typedef std::shared_ptr DnsQueryPtr; } diff --git a/evpp/EventLoop.h b/evpp/EventLoop.h index 5e7da9552..fa8128efe 100644 --- a/evpp/EventLoop.h +++ b/evpp/EventLoop.h @@ -36,6 +36,7 @@ class EventLoop : public Status { } connectionNum = 0; nextTimerID = 0; + nextDnsID = 0; setStatus(kInitialized); } @@ -153,6 +154,34 @@ class EventLoop : public Status { return hloop_tid(loop_); } + // DNS interfaces: resolveDns, cancelDns (mirror setTimer/killTimer). + // resolveDns returns a use-after-free-proof DnsID: the EventLoop keeps a + // DnsID -> DnsQuery map, and a stale id (completed/cancelled) makes + // cancelDns() a safe no-op. @cb runs in the loop thread. + // NOTE: must be called from the loop thread. + DnsID resolveDns(const char* host, DnsCallback cb, const hdns_setting_t* opt = NULL) { + if (loop_ == NULL) return INVALID_DNS_ID; + assertInLoopThread(); + DnsID dnsID = generateDnsID(); + hdns_t* q = hdns_resolve_ex(loop_, host, opt, onDnsResolved, this); + if (q == NULL) return INVALID_DNS_ID; + // stash the DnsID in the query's event_id so completion can find us. + hevent_set_id(q, dnsID); + dns_queries[dnsID] = std::make_shared(q, std::move(cb)); + return dnsID; + } + + // cancelDns thread-safe + void cancelDns(DnsID dnsID) { + runInLoop([dnsID, this]() { + auto iter = dns_queries.find(dnsID); + if (iter != dns_queries.end()) { + if (iter->second->query) hdns_cancel(iter->second->query); + dns_queries.erase(iter); + } + }); + } + bool isInLoopThread() { if (loop_ == NULL) return false; return hv_gettid() == hloop_tid(loop_); @@ -196,6 +225,11 @@ class EventLoop : public Status { return (((TimerID)tid() & 0xFFFFFFFF) << 32) | ++nextTimerID; } + DnsID generateDnsID() { + // start at 1; 0 == INVALID_DNS_ID. 64-bit monotonic, never reused. + return ++nextDnsID; + } + static void onTimer(htimer_t* htimer) { EventLoop* loop = (EventLoop*)hevent_userdata(htimer); @@ -228,6 +262,22 @@ class EventLoop : public Status { if (ev && ev->cb) ev->cb(ev.get()); } + // C hdns completion callback (runs in loop thread). userdata == EventLoop*. + // The DnsID rode along in the query's event_id (set in resolveDns), so we + // recover it from the handle, deliver to the C++ callback, and erase the + // map entry. Erase BEFORE invoking the user callback so a re-entrant + // cancelDns() for this id safely no-ops. The hdns_t is freed by the C + // resolver right after this returns; we never touch it afterwards. + static void onDnsResolved(hdns_t* query, const hdns_result_t* result, void* userdata) { + EventLoop* loop = (EventLoop*)userdata; + DnsID dnsID = (DnsID)hevent_id(query); + auto iter = loop->dns_queries.find(dnsID); + if (iter == loop->dns_queries.end()) return; + DnsCallback cb = std::move(iter->second->cb); + loop->dns_queries.erase(iter); + if (cb) cb(result->status, result->naddrs, result->addrs); + } + public: std::atomic connectionNum; // for LB_LeastConnections private: @@ -237,6 +287,8 @@ class EventLoop : public Status { std::queue customEvents; // GUAREDE_BY(mutex_) std::map timers; std::atomic nextTimerID; + std::map dns_queries; + std::atomic nextDnsID; }; typedef std::shared_ptr EventLoopPtr; diff --git a/evpp/TcpClient.h b/evpp/TcpClient.h index fb6391211..1e2d61317 100644 --- a/evpp/TcpClient.h +++ b/evpp/TcpClient.h @@ -4,6 +4,8 @@ #include "hsocket.h" #include "hssl.h" #include "hlog.h" +#include "herr.h" +#include "hdns.h" #include "EventLoopThread.h" #include "Channel.h" @@ -27,10 +29,12 @@ class TcpClientEventLoopTmpl { reconn_setting = NULL; unpack_setting = NULL; reconn_timer_id = INVALID_TIMER_ID; + dns_id = INVALID_DNS_ID; } virtual ~TcpClientEventLoopTmpl() { cancelReconnectTimer(); + cancelDnsQuery(); HV_FREE(tls_setting); HV_FREE(reconn_setting); HV_FREE(unpack_setting); @@ -49,16 +53,39 @@ class TcpClientEventLoopTmpl { } // NOTE: By default, not bind local port. If necessary, you can call bind() after createsocket(). - // @retval >=0 connfd, <0 error + // + // @retval >0 connfd: the socket was created immediately (remote_host is a + // numeric IP, or a Unix Domain Socket when remote_port < 0). + // @retval =0 pending: remote_host is a hostname, so socket creation is + // deferred until the address is resolved asynchronously in + // startConnect() (avoids blocking the event loop on getaddrinfo). + // This is NOT an error; the real fd is available later via + // channel->fd(). Callers should treat >=0 as success and only + // <0 as failure. + // @retval <0 error. + // + // Rationale for the =0 case: a socket's address family (AF_INET vs AF_INET6) + // must be known before socket() is called, but for a hostname the family is + // only known after DNS returns an A (IPv4) or AAAA (IPv6) record. So the + // socket cannot be created up front and is created once resolution completes + // in startConnectWithAddr() -- the same order as the standard getaddrinfo + // connect loop. int createsocket(int remote_port, const char* remote_host = "127.0.0.1") { - memset(&remote_addr, 0, sizeof(remote_addr)); - int ret = sockaddr_set_ipport(&remote_addr, remote_host, remote_port); - if (ret != 0) { - return NABS(ret); - } this->remote_host = remote_host; this->remote_port = remote_port; - return createsocket(&remote_addr.sa); + memset(&remote_addr, 0, sizeof(remote_addr)); + // numeric IP (or UDS: port < 0) -> resolve now (non-blocking) and + // create the socket immediately, returning the connfd (>0). + if (remote_port < 0 || is_ipaddr(remote_host)) { + int ret = sockaddr_set_ipport(&remote_addr, remote_host, remote_port); + if (ret != 0) { + return NABS(ret); + } + return createsocket(&remote_addr.sa); + } + // hostname -> defer socket creation to async resolution in + // startConnect(); return 0 (pending), not an error. + return 0; } int createsocket(struct sockaddr* remote_addr) { @@ -101,6 +128,7 @@ class TcpClientEventLoopTmpl { void closesocket() { if (channel && channel->status != SocketChannel::CLOSED) { loop_->runInLoop([this](){ + cancelDnsQuery(); if (channel) { setReconnect(NULL); channel->close(); @@ -111,14 +139,100 @@ class TcpClientEventLoopTmpl { int startConnect() { loop_->assertInLoopThread(); - if (channel == NULL || channel->isClosed()) { - int connfd = -1; - if (reconn_setting && reconn_setting->cur_retry_cnt > 1) { - // Resolve DNS to get the latest IP address - connfd = createsocket(remote_port, remote_host.c_str()); - } else { - connfd = createsocket(&remote_addr.sa); + // If the target is a hostname, resolve it asynchronously through hdns + // so the event loop is never blocked by getaddrinfo. This covers both + // the first connect and every reconnect (to pick up DNS changes). + // Numeric-IP targets skip resolution and connect directly. + // NOTE: any TcpClientTmpl subclass (WebSocketClient, ...) gets this for + // free; no per-client DNS glue is needed. + // NOTE: Unix Domain Socket targets (remote_port < 0) carry a filesystem + // path in remote_host, not a hostname; remote_addr is already set by + // createsocket(), so never run DNS on them. + if (remote_port >= 0 && !remote_host.empty() && !is_ipaddr(remote_host.c_str())) { + return startResolveThenConnect(); + } + return startConnectWithAddr(); + } + + // @internal: resolve remote_host asynchronously, then connect. + // Uses EventLoop::resolveDns which returns a use-after-free-proof DnsID and + // manages the underlying hdns_t lifetime, so this class only holds an id. + int startResolveThenConnect() { + cancelDnsQuery(); + hdns_setting_t opt; + opt.family = HDNS_QUERY_BOTH; + if (connect_timeout > 0) opt.timeout_ms = connect_timeout; + dns_id = loop_->resolveDns(remote_host.c_str(), + [this](int status, int naddrs, const sockaddr_u* addrs) { + dns_id = INVALID_DNS_ID; // this query is done + if (status == HDNS_STATUS_OK && naddrs > 0) { + // adopt the first resolved address, keep the target port + remote_addr = addrs[0]; + sockaddr_set_port(&remote_addr, remote_port); + } else if (remote_addr.sa.sa_family == 0) { + // resolve failed and no previously-resolved address to fall + // back to. Report failure and drive reconnect (if enabled). + hloge("resolve %s failed, status=%d", remote_host.c_str(), status); + onDnsResolveFailed(); + return; + } + // else: resolve failed but keep the previous remote_addr; the + // connect attempt will fail and drive the normal reconnect path. + startConnectWithAddr(); + }, &opt); + if (dns_id == INVALID_DNS_ID) { + // Could not start async resolve. If we already have a usable address + // (a previous resolve), fall back to a direct connect; otherwise + // (hostname first attempt, remote_addr unset) report a DNS failure + // rather than calling socket(0, ...) with an unset family. + if (remote_addr.sa.sa_family == 0) { + onDnsResolveFailed(); + return 0; } + return startConnectWithAddr(); + } + return 0; + } + + // @internal: DNS resolution failed with no usable address. + // Ensure the user is always notified (even on the very first attempt, when + // no channel exists yet). onConnection users expect a valid (non-null) + // channel, so create a NULL-io channel: it reports isConnected()==false and + // all its methods guard against a null io, matching the connect-failure + // contract without fabricating a real socket. + void onDnsResolveFailed() { + if (channel == NULL) { + channel = std::make_shared((hio_t*)NULL); + } + // record the reason so the user can distinguish it via channel->error() + // (there is no io_ to carry the error on the first-attempt path). + channel->setError(ERR_DNS_RESOLVE); + notifyDisconnectThenReconnect(); + } + + // @internal: notify a disconnect via onConnection, then reconnect if set. + // Snapshots the reconnect flag before the callback so a user that disables + // reconnect (setReconnect(NULL)) inside onConnection still won't reconnect. + // CONTRACT: when reconnect is enabled, the user must NOT destroy this client + // from within onConnection -- startReconnect() runs after the callback and + // is a member call, so destroying here would be a use-after-free. To tear + // down from onConnection, call setReconnect(NULL) first (or defer the + // destroy, e.g. via deleteInLoop()). This matches the long-standing evpp + // onclose behavior; shared by the onclose path and the DNS-failure path. + void notifyDisconnectThenReconnect() { + bool reconnect = reconn_setting != NULL; + if (onConnection) { + onConnection(channel); + } + if (reconnect) { + startReconnect(); + } + } + + int startConnectWithAddr() { + loop_->assertInLoopThread(); + if (channel == NULL || channel->isClosed()) { + int connfd = createsocket(&remote_addr.sa); if (connfd < 0) { hloge("createsocket %s:%d return %d!\n", remote_host.c_str(), remote_port, connfd); return connfd; @@ -167,13 +281,7 @@ class TcpClientEventLoopTmpl { } }; channel->onclose = [this]() { - bool reconnect = reconn_setting != NULL; - if (onConnection) { - onConnection(channel); - } - if (reconnect) { - startReconnect(); - } + notifyDisconnectThenReconnect(); }; return channel->startConnect(); } @@ -283,9 +391,18 @@ class TcpClientEventLoopTmpl { } } + // Cancel any in-flight async DNS query (loop thread only). + void cancelDnsQuery() { + if (dns_id != INVALID_DNS_ID) { + loop_->cancelDns(dns_id); + dns_id = INVALID_DNS_ID; + } + } + private: EventLoopPtr loop_; TimerID reconn_timer_id; + DnsID dns_id; }; template diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 422e61eb4..324030d3f 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -12,6 +12,7 @@ list(APPEND EXAMPLES udp_echo_server udp_proxy_server socks5_proxy_server + host multi-acceptor-processes multi-acceptor-threads one-acceptor-multi-workers @@ -60,6 +61,9 @@ target_link_libraries(udp_proxy_server ${HV_LIBRARIES}) add_executable(socks5_proxy_server socks5_proxy_server.c) target_link_libraries(socks5_proxy_server ${HV_LIBRARIES}) +add_executable(host host.c) +target_link_libraries(host ${HV_LIBRARIES}) + add_executable(multi-acceptor-processes multi-thread/multi-acceptor-processes.c) target_link_libraries(multi-acceptor-processes ${HV_LIBRARIES}) diff --git a/examples/host.c b/examples/host.c new file mode 100644 index 000000000..470828c23 --- /dev/null +++ b/examples/host.c @@ -0,0 +1,62 @@ +/* + * host — asynchronous DNS resolve tool (like the system `host` command). + * + * Usage: host [domain] [domain2 ...] + * e.g. host www.example.com github.com localhost 1.1.1.1 + * + * Resolves each domain asynchronously through the event loop (no blocking + * getaddrinfo) via hdns_resolve() and prints the resulting addresses. Quits + * when all queries have completed. + */ + +#include "hloop.h" +#include "hdns.h" +#include "hsocket.h" +#include "hbase.h" + +static int pending = 0; + +static void on_resolved(hdns_t* query, const hdns_result_t* result, void* userdata) { + (void)query; + hloop_t* loop = (hloop_t*)userdata; + if (result->status == HDNS_STATUS_OK) { + printf("%s =>\n", result->host); + for (int i = 0; i < result->naddrs; ++i) { + char ip[SOCKADDR_STRLEN] = {0}; + // cast away const: sockaddr_ip only reads + sockaddr_ip((sockaddr_u*)&result->addrs[i], ip, sizeof(ip)); + printf(" %s\n", ip); + } + } else { + printf("%s => resolve failed, status=%d\n", result->host, result->status); + } + + if (--pending == 0) { + hloop_stop(loop); + } +} + +int main(int argc, char** argv) { + const char* default_hosts[] = { "localhost", "www.example.com", "github.com" }; + const char** hosts; + int nhosts; + if (argc > 1) { + hosts = (const char**)(argv + 1); + nhosts = argc - 1; + } else { + hosts = default_hosts; + nhosts = (int)(sizeof(default_hosts) / sizeof(default_hosts[0])); + printf("usage: %s domain [domain2 ...]\n", argv[0]); + printf("(no args given, resolving defaults)\n\n"); + } + + hloop_t* loop = hloop_new(0); + pending = nhosts; + for (int i = 0; i < nhosts; ++i) { + hdns_resolve(loop, hosts[i], on_resolved, loop); + } + + hloop_run(loop); + hloop_free(&loop); + return 0; +} diff --git a/examples/http_client_test.cpp b/examples/http_client_test.cpp index 5d7bbc461..8f8af3120 100644 --- a/examples/http_client_test.cpp +++ b/examples/http_client_test.cpp @@ -17,7 +17,7 @@ static void test_http_async_client(HttpClient* cli, int* resp_cnt) { printf("test_http_async_client request thread tid=%ld\n", hv_gettid()); auto req = std::make_shared(); req->method = HTTP_POST; - req->url = "http://127.0.0.1:8080/echo"; + req->url = "http://localhost:8080/echo"; req->headers["Connection"] = "keep-alive"; req->body = "This is an async request."; req->timeout = 10; diff --git a/http/client/AsyncHttpClient.cpp b/http/client/AsyncHttpClient.cpp index f20d7cc8c..5cac31634 100644 --- a/http/client/AsyncHttpClient.cpp +++ b/http/client/AsyncHttpClient.cpp @@ -34,14 +34,64 @@ int AsyncHttpClient::doTask(const HttpClientTaskPtr& task) { } req->ParseUrl(); - sockaddr_u peeraddr; - memset(&peeraddr, 0, sizeof(peeraddr)); const char* host = req->host.c_str(); - int ret = sockaddr_set_ipport(&peeraddr, host, req->port); - if (ret != 0) { - hloge("unknown host %s", host); + + // If host is a numeric IP (or UDS), resolve synchronously (fast path). + // Otherwise resolve the hostname asynchronously via EventLoop::resolveDns + // so the event loop is never blocked by getaddrinfo. resolveDns returns a + // use-after-free-proof DnsID and owns the underlying hdns_t lifetime. + if (req->port < 0 || is_ipaddr(host)) { + sockaddr_u peeraddr; + memset(&peeraddr, 0, sizeof(peeraddr)); + int ret = sockaddr_set_ipport(&peeraddr, host, req->port); + if (ret != 0) { + hloge("unknown host %s", host); + return -20; + } + return doTaskWithAddr(task, &peeraddr); + } + + hdns_setting_t opt; + if (req->connect_timeout > 0) opt.timeout_ms = req->connect_timeout * 1000; + int port = req->port; + DnsID id = EventLoopThread::loop()->resolveDns(host, + [this, task, port](int status, int naddrs, const sockaddr_u* addrs) { + if (status != HDNS_STATUS_OK || naddrs <= 0) { + hloge("resolve host %s failed, status=%d", task->req->host.c_str(), status); + if (task->cb) task->cb(NULL); + return; + } + sockaddr_u peeraddr = addrs[0]; + sockaddr_set_port(&peeraddr, port); + int err = doTaskWithAddr(task, &peeraddr); + if (err != 0 && task->cb) { + task->cb(NULL); + } + }, &opt); + if (id == INVALID_DNS_ID) { + hloge("hdns_resolve failed for host %s", host); return -20; } + return 0; +} + +// Continue the request once the peer address is known. +int AsyncHttpClient::doTaskWithAddr(const HttpClientTaskPtr& task, const sockaddr_u* paddr) { + const HttpRequestPtr& req = task->req; + if (req->cancel) { + return -1; + } + + uint64_t now_hrtime = hloop_now_hrtime(EventLoopThread::hloop()); + int elapsed_ms = (now_hrtime - task->start_time) / 1000; + int timeout_ms = req->timeout * 1000; + if (timeout_ms > 0 && elapsed_ms >= timeout_ms) { + hlogw("%s queueInLoop timeout!", req->url.c_str()); + return -10; + } + + const char* host = req->host.c_str(); + sockaddr_u peeraddr = *paddr; int connfd = -1; // first get from conn_pools diff --git a/http/client/AsyncHttpClient.h b/http/client/AsyncHttpClient.h index 38ff4fea7..31fd262fb 100644 --- a/http/client/AsyncHttpClient.h +++ b/http/client/AsyncHttpClient.h @@ -110,6 +110,9 @@ class HV_EXPORT AsyncHttpClient : private EventLoopThread { } } ~AsyncHttpClient() { + // Stop (and, for an owned loop, free) the event loop. Any in-flight + // async DNS queries are owned by EventLoop::resolveDns and the C + // resolver, which are torn down with the loop; nothing to clean here. EventLoopThread::stop(true); } @@ -129,6 +132,9 @@ class HV_EXPORT AsyncHttpClient : private EventLoopThread { } int doTask(const HttpClientTaskPtr& task); + // @internal: continue doTask after the peer address is known. + int doTaskWithAddr(const HttpClientTaskPtr& task, const sockaddr_u* peeraddr); + static int sendRequest(const SocketChannelPtr& channel); // channel diff --git a/scripts/unittest.sh b/scripts/unittest.sh index dec6d3c87..f32edee13 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -32,6 +32,12 @@ bin/socketpair_test # bin/objectpool_test bin/sizeof_test bin/http_router_test +if [ -x bin/hdns_test ]; then + bin/hdns_test +fi +if [ -x bin/tcpclient_dns_test ]; then + bin/tcpclient_dns_test +fi for redis_test in redis_async_client_test redis_client_test redis_batch_test redis_subscriber_test; do if [ -x bin/${redis_test} ]; then bin/${redis_test} diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index db466c0fb..c92a3e1fb 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -90,6 +90,15 @@ target_include_directories(sendmail PRIVATE .. ../base ../protocol ../util) add_executable(http_router_test http_router_test.cpp) target_include_directories(http_router_test PRIVATE ../http/server) +# ------event: async dns------ +add_executable(hdns_test hdns_test.c) +target_include_directories(hdns_test PRIVATE .. ../base ../ssl ../event) +target_link_libraries(hdns_test ${HV_LIBRARIES}) + +add_executable(hdns_benchmark hdns_benchmark.c) +target_include_directories(hdns_benchmark PRIVATE .. ../base ../ssl ../event) +target_link_libraries(hdns_benchmark ${HV_LIBRARIES}) + # ------redis------ if(WITH_EVPP AND WITH_REDIS) add_executable(redis_protocol_test redis_protocol_test.cpp ../redis/RedisMessage.cpp) @@ -108,6 +117,14 @@ endforeach() list(APPEND REDIS_UNITTEST_TARGETS ${REDIS_LINKED_UNITTEST_TARGETS}) endif() +# ------evpp: async dns in connect path (connect/reconnect, lifetime, resolve-fail)------ +if(WITH_EVPP) +add_executable(tcpclient_dns_test tcpclient_dns_test.cpp) +target_include_directories(tcpclient_dns_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp) +target_link_libraries(tcpclient_dns_test ${HV_LIBRARIES}) +set(EVPP_DNS_UNITTEST_TARGETS tcpclient_dns_test) +endif() + if(UNIX) add_executable(webbench webbench.c) endif() @@ -138,5 +155,8 @@ add_custom_target(unittest DEPENDS ftp sendmail http_router_test + hdns_test + hdns_benchmark ${REDIS_UNITTEST_TARGETS} + ${EVPP_DNS_UNITTEST_TARGETS} ) diff --git a/unittest/hdns_benchmark.c b/unittest/hdns_benchmark.c new file mode 100644 index 000000000..753497e8d --- /dev/null +++ b/unittest/hdns_benchmark.c @@ -0,0 +1,106 @@ +/* + * hdns_benchmark — compare blocking getaddrinfo() vs async hdns_resolve(). + * + * Resolves a list of hostnames two ways and reports wall-clock time: + * 1. Blocking: sequential getaddrinfo() (what ResolveAddr uses today). + * 2. Async: all hostnames resolved concurrently via hdns on one loop. + * + * The async path issues all queries up front and lets the single event loop + * multiplex them, so total time is roughly one slow lookup rather than the + * sum of all lookups. Crucially, the event loop is never blocked. + * + * Usage: hdns_benchmark [host1 host2 ...] + * + * NOTE: results depend on your network / DNS cache. Run a couple of times. + */ + +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +#include "hloop.h" +#include "hdns.h" +#include "hsocket.h" +#include "htime.h" + +static const char* default_hosts[] = { + "www.example.com", "github.com", "www.google.com", "www.cloudflare.com", + "www.wikipedia.org", "www.baidu.com", "www.qq.com", "www.taobao.com", +}; + +static int g_pending = 0; +static int g_ok = 0; + +static void on_resolved(hdns_t* query, const hdns_result_t* result, void* userdata) { + (void)query; + hloop_t* loop = (hloop_t*)userdata; + if (result->status == HDNS_STATUS_OK) ++g_ok; + if (--g_pending == 0) hloop_stop(loop); +} + +int main(int argc, char** argv) { + const char** hosts; + int nhosts; + if (argc > 1) { + hosts = (const char**)(argv + 1); + nhosts = argc - 1; + } else { + hosts = default_hosts; + nhosts = (int)(sizeof(default_hosts) / sizeof(default_hosts[0])); + } + +#ifdef _WIN32 + WSADATA wsa; WSAStartup(MAKEWORD(2, 2), &wsa); +#endif + + printf("Resolving %d hostnames.\n\n", nhosts); + + // -------- 1. blocking getaddrinfo (sequential) -------- + unsigned long long t0 = gethrtime_us(); + int blk_ok = 0; + for (int i = 0; i < nhosts; ++i) { + struct addrinfo* ai = NULL; + int ret = getaddrinfo(hosts[i], NULL, NULL, &ai); + if (ret == 0 && ai) { ++blk_ok; freeaddrinfo(ai); } + } + unsigned long long t1 = gethrtime_us(); + double blk_ms = (t1 - t0) / 1000.0; + printf("[blocking getaddrinfo] %d/%d ok in %.1f ms (sequential)\n", + blk_ok, nhosts, blk_ms); + + // -------- 2. async hdns (concurrent on one loop) -------- + hloop_t* loop = hloop_new(0); + g_pending = nhosts; + g_ok = 0; + unsigned long long a0 = gethrtime_us(); + for (int i = 0; i < nhosts; ++i) { + // disable cache so the comparison is apples-to-apples (real queries) + hdns_setting_t opt; + memset(&opt, 0, sizeof(opt)); + opt.family = HDNS_QUERY_A; + opt.timeout_ms = 5000; + opt.retries = 2; + opt.use_cache = 0; + hdns_resolve_ex(loop, hosts[i], &opt, on_resolved, loop); + } + hloop_run(loop); + unsigned long long a1 = gethrtime_us(); + double async_ms = (a1 - a0) / 1000.0; + printf("[async hdns] %d/%d ok in %.1f ms (concurrent)\n", + g_ok, nhosts, async_ms); + + printf("\nspeedup: %.2fx (async vs sequential blocking)\n", + async_ms > 0 ? blk_ms / async_ms : 0.0); + printf("NOTE: the real win is that the event loop is never blocked during\n" + " async resolution, so other IO/timers keep running.\n"); + + hloop_free(&loop); + return 0; +} diff --git a/unittest/hdns_test.c b/unittest/hdns_test.c new file mode 100644 index 000000000..5a92c267f --- /dev/null +++ b/unittest/hdns_test.c @@ -0,0 +1,243 @@ +/* + * hdns_test — unit test for the asynchronous DNS resolver (event/hdns.*). + * + * Tests run deterministically without requiring external network access by + * spinning up a local mock DNS nameserver (a UDP socket answering with canned + * A records) and pointing the resolver at it via hdns_setting_t.nameserver. + * + * Covered: + * 1. numeric IPv4 / IPv6 fast path + * 2. /etc/hosts lookup (localhost) + * 3. real query round-trip against a mock nameserver (wire build + parse) + * 4. cache hit (second resolve does not hit the mock server) + * 5. cancel before completion (callback not invoked) + * 6. NXDOMAIN handling + */ + +#include +#include +#include + +#include "hloop.h" +#include "hdns.h" +#include "hsocket.h" +#include "hbase.h" +#include "htime.h" + +//------------------------------------------------------------------------------ +// Mock DNS nameserver: parses the incoming question and replies with A records +// based on a tiny built-in zone. Runs in the same loop. +//------------------------------------------------------------------------------ + +static int g_mock_queries = 0; // count of questions received by the mock +static int g_mock_port = 0; + +// Build a minimal DNS response for the given request buffer. +// Answers with 93.184.216.34 for any A query except "nx.test" (NXDOMAIN). +static int mock_build_response(const uint8_t* req, int reqlen, uint8_t* resp, int resplen) { + if (reqlen < 12) return -1; + // copy header, set QR=1, RA=1 + memcpy(resp, req, 12); + resp[2] = 0x81; // QR=1, RD=1 + resp[3] = 0x80; // RA=1, rcode=0 + + // parse question name to detect nx.test + int off = 12; + char name[256] = {0}; + int npos = 0; + while (off < reqlen) { + uint8_t len = req[off]; + if (len == 0) { off += 1; break; } + if ((len & 0xC0) != 0) return -1; + if (off + 1 + len > reqlen) return -1; + if (npos > 0) name[npos++] = '.'; + memcpy(name + npos, req + off + 1, len); + npos += len; + off += 1 + len; + } + if (off + 4 > reqlen) return -1; + uint16_t qtype = (req[off] << 8) | req[off + 1]; + off += 4; // qtype + qclass + int qend = off; + + int is_nx = (strcasecmp(name, "nx.test") == 0); + int answer_a = (qtype == 1 && !is_nx); // only answer A here + + // counts + resp[4] = req[4]; resp[5] = req[5]; // qdcount = 1 + if (is_nx) { + resp[3] = 0x83; // rcode = 3 NXDOMAIN + resp[6] = 0; resp[7] = 0; + } else { + resp[6] = 0; resp[7] = (uint8_t)(answer_a ? 1 : 0); // ancount + } + resp[8] = 0; resp[9] = 0; + resp[10] = 0; resp[11] = 0; + + // copy question section verbatim + if (qend > resplen) return -1; + memcpy(resp + 12, req + 12, qend - 12); + int roff = qend; + + if (answer_a) { + // answer: name pointer -> 0xC00C, type A, class IN, ttl 60, rdlen 4 + if (roff + 16 > resplen) return -1; + resp[roff++] = 0xC0; resp[roff++] = 0x0C; // ptr to qname at offset 12 + resp[roff++] = 0x00; resp[roff++] = 0x01; // type A + resp[roff++] = 0x00; resp[roff++] = 0x01; // class IN + resp[roff++] = 0x00; resp[roff++] = 0x00; + resp[roff++] = 0x00; resp[roff++] = 0x3C; // ttl 60 + resp[roff++] = 0x00; resp[roff++] = 0x04; // rdlen 4 + resp[roff++] = 93; resp[roff++] = 184; + resp[roff++] = 216; resp[roff++] = 34; + } + return roff; +} + +static void mock_on_recv(hio_t* io, void* buf, int readbytes) { + ++g_mock_queries; + uint8_t resp[512]; + int rlen = mock_build_response((const uint8_t*)buf, readbytes, resp, sizeof(resp)); + if (rlen > 0) { + // reply to sender (peeraddr was set by recvfrom) + hio_sendto(io, resp, rlen, hio_peeraddr(io)); + } +} + +static hio_t* start_mock_nameserver(hloop_t* loop) { + // bind an ephemeral UDP port on 127.0.0.1 + hio_t* io = hloop_create_udp_server(loop, "127.0.0.1", 0); + assert(io != NULL); + struct sockaddr* la = hio_localaddr(io); + g_mock_port = ntohs(((struct sockaddr_in*)la)->sin_port); + hio_setcb_read(io, mock_on_recv); + hio_read(io); + return io; +} + +//------------------------------------------------------------------------------ +// Test harness: each test sets an expectation and stops the loop when met. +//------------------------------------------------------------------------------ + +typedef struct { + const char* label; + int got; + int want_status; + int want_min_addrs; + hloop_t* loop; +} expect_t; + +static void on_expect(hdns_t* query, const hdns_result_t* result, void* userdata) { + (void)query; + expect_t* e = (expect_t*)userdata; + e->got = 1; + printf("[%s] status=%d naddrs=%d\n", e->label, result->status, result->naddrs); + assert(result->status == e->want_status); + if (result->status == HDNS_STATUS_OK) { + assert(result->naddrs >= e->want_min_addrs); + } + hloop_stop(e->loop); +} + +static void run_expect(hloop_t* loop, const char* host, const hdns_setting_t* opt, + int want_status, int want_min_addrs) { + expect_t e; + memset(&e, 0, sizeof(e)); + e.label = host; + e.want_status = want_status; + e.want_min_addrs = want_min_addrs; + e.loop = loop; + hdns_t* q = hdns_resolve_ex(loop, host, opt, on_expect, &e); + assert(q != NULL); + hloop_run(loop); + assert(e.got == 1); +} + +// callback that must NEVER be invoked (cancel test) +static int g_cancel_cb_called = 0; +static void on_never(hdns_t* query, const hdns_result_t* result, void* userdata) { + (void)query; (void)result; (void)userdata; + g_cancel_cb_called = 1; +} +static void stop_after(htimer_t* timer) { + hloop_stop(hevent_loop(timer)); +} + +int main() { + hloop_t* loop = hloop_new(0); + + hio_t* mock = start_mock_nameserver(loop); + char ns[32]; + snprintf(ns, sizeof(ns), "127.0.0.1:%d", g_mock_port); + printf("mock nameserver on %s\n", ns); + + hdns_setting_t opt; + memset(&opt, 0, sizeof(opt)); + opt.family = HDNS_QUERY_A; // only A (mock answers A) + opt.timeout_ms = 2000; + opt.retries = 1; + opt.use_cache = 1; + opt.nameserver = ns; + + // 1) numeric IPv4 fast path (no nameserver needed) + run_expect(loop, "8.8.4.4", &opt, HDNS_STATUS_OK, 1); + + // 2) numeric IPv6 fast path + { + hdns_setting_t o6 = opt; + o6.family = HDNS_QUERY_BOTH; + run_expect(loop, "::1", &o6, HDNS_STATUS_OK, 1); + } + + // 3) /etc/hosts: localhost should resolve without touching the mock + { + int before = g_mock_queries; + hdns_setting_t oh = opt; + oh.family = HDNS_QUERY_BOTH; + run_expect(loop, "localhost", &oh, HDNS_STATUS_OK, 1); + assert(g_mock_queries == before); // hosts hit, no query sent + } + + // 4) real query round-trip against the mock nameserver + { + int before = g_mock_queries; + run_expect(loop, "example.test", &opt, HDNS_STATUS_OK, 1); + assert(g_mock_queries > before); // a question reached the mock + } + + // 5) cache hit: second resolve of same host must not query the mock again + { + int before = g_mock_queries; + run_expect(loop, "example.test", &opt, HDNS_STATUS_OK, 1); + assert(g_mock_queries == before); // served from cache + } + + // 6) NXDOMAIN + { + hdns_setting_t onx = opt; + onx.use_cache = 0; + run_expect(loop, "nx.test", &onx, HDNS_STATUS_NXDOMAIN, 0); + } + + // 7) cancel before completion: callback must not fire + { + g_cancel_cb_called = 0; + hdns_setting_t oc = opt; + oc.use_cache = 0; + // use a host the mock does not answer quickly? It answers immediately, + // so cancel synchronously right after issuing, before the deferred send. + hdns_t* q = hdns_resolve_ex(loop, "cancel.test", &oc, on_never, NULL); + assert(q != NULL); + hdns_cancel(q); + // spin the loop briefly to ensure no callback is delivered + htimer_add(loop, stop_after, 200, 1); + hloop_run(loop); + assert(g_cancel_cb_called == 0); + printf("[cancel.test] callback correctly NOT called\n"); + } + + hio_close(mock); + hloop_free(&loop); + printf("\nALL hdns_test PASSED\n"); + return 0; +} diff --git a/unittest/tcpclient_dns_test.cpp b/unittest/tcpclient_dns_test.cpp new file mode 100644 index 000000000..9e807fc55 --- /dev/null +++ b/unittest/tcpclient_dns_test.cpp @@ -0,0 +1,172 @@ +/* + * tcpclient_dns_test — integration tests for async DNS in the evpp connect path. + * + * Covers three scenarios (all no external network required): + * 1. connect: TcpClient with a *hostname* target ("localhost", resolved via + * /etc/hosts) connects, and reconnect re-resolves the hostname through the + * async resolver without blocking the loop. + * 2. lifetime: destroying a TcpClient while a hostname resolve is still in + * flight must be clean (no use-after-free / double-free). This exercises + * the DnsID handle model (EventLoop keeps a DnsID -> query map; a stale id + * no-ops on cancel). + * 3. resolvefail: a first-attempt resolve failure (before any channel exists) + * must still notify onConnection with a valid, non-connected channel whose + * error() reports ERR_DNS_RESOLVE. + */ + +#include +#include +#include + +#include "TcpServer.h" +#include "TcpClient.h" +#include "EventLoop.h" +#include "herr.h" +#include "htime.h" + +using namespace hv; + +// 1. hostname connect + async re-resolve on reconnect +static int test_connect_and_reconnect() { + std::atomic connect_count{0}; + std::atomic reconnected{false}; + + int port = 40833; + TcpServer srv; + int listenfd = srv.createsocket(port, "127.0.0.1"); + if (listenfd < 0) { + printf("[connect] createsocket failed on port %d\n", port); + return -1; + } + srv.onMessage = [](const SocketChannelPtr& ch, Buffer* buf) { + ch->write(buf); // echo + }; + srv.start(); + + // TcpClient targeting "localhost" (a hostname -> exercises DNS path) + auto cli = std::make_shared(); + int connfd = cli->createsocket(port, "localhost"); + if (connfd < 0) { + printf("[connect] client createsocket failed\n"); + return -1; + } + + reconn_setting_t reconn; + reconn_setting_init(&reconn); + reconn.min_delay = 100; + reconn.max_delay = 500; + reconn.max_retry_cnt = 10; + cli->setReconnect(&reconn); + + cli->onConnection = [&](const SocketChannelPtr& channel) { + if (channel->isConnected()) { + int n = ++connect_count; + printf("[connect] connected to %s (count=%d)\n", channel->peeraddr().c_str(), n); + if (n == 1) { + // force a disconnect shortly to trigger reconnect + re-resolve. + setTimeout(200, [channel](TimerID) { + channel->close(); + }); + } else if (n >= 2) { + reconnected = true; + } + } + }; + cli->start(); + + uint64_t start = gettick_ms(); + while (!reconnected && gettick_ms() - start < 8000) { + hv_msleep(50); + } + + cli->setReconnect(NULL); + cli->stop(true); + srv.stop(); + hv_msleep(200); + + if (connect_count >= 2 && reconnected) { + printf("[connect] PASS: hostname connect + async re-resolve reconnect (connects=%d)\n", + connect_count.load()); + return 0; + } + printf("[connect] FAIL: connects=%d reconnected=%d\n", + connect_count.load(), (int)reconnected); + return -1; +} + +// 2. destroying a client mid-resolve must not UAF / double-free +static int test_destroy_mid_resolve() { + // Repeated teardown while a hostname resolve is (very likely) still in flight. + // Use .invalid (RFC 6761: never resolves) so the query stays pending/fails. + for (int i = 0; i < 20; ++i) { + auto cli = std::make_shared(); + cli->createsocket(80, "nonexistent-host.invalid"); + cli->start(); + hv_msleep(i % 3); // vary the teardown window: 0/1/2 ms + cli->stop(); + cli.reset(); // destroy client mid-resolve + } + for (int i = 0; i < 20; ++i) { + auto cli = std::make_shared(); + cli->createsocket(80, "another-missing-host.invalid"); + cli->start(); + cli->stop(); + cli.reset(); + } + printf("[lifetime] PASS: destroying TcpClient mid-resolve is clean (no UAF/double-free)\n"); + return 0; +} + +// 3. first-attempt resolve failure must notify onConnection with ERR_DNS_RESOLVE +static int test_resolve_failure_notifies() { + std::atomic got_disconnected_cb{false}; + std::atomic saw_connected{false}; + std::atomic reported_error{0}; + + auto cli = std::make_shared(); + // unresolvable hostname (RFC 6761 .invalid), NO reconnect configured. + cli->createsocket(80, "definitely-nonexistent.invalid"); + cli->onConnection = [&](const SocketChannelPtr& channel) { + // user code commonly calls channel->isConnected()/peeraddr() first, + // so a nullptr channel would crash; a NULL-io channel must be safe. + if (channel && channel->isConnected()) { + saw_connected = true; + } else { + (void)channel->peeraddr(); // must be safe on a NULL-io channel + reported_error = channel->error(); // must be ERR_DNS_RESOLVE + got_disconnected_cb = true; + } + }; + cli->start(); + + uint64_t start = gettick_ms(); + while (!got_disconnected_cb && gettick_ms() - start < 8000) { + hv_msleep(20); + } + cli->stop(); + hv_msleep(100); + + if (got_disconnected_cb && !saw_connected && reported_error == ERR_DNS_RESOLVE) { + printf("[resolvefail] PASS: resolve failure notifies onConnection (error=%d %s)\n", + reported_error.load(), hv_strerror(reported_error)); + return 0; + } + printf("[resolvefail] FAIL: got_cb=%d connected=%d error=%d (want %d)\n", + (int)got_disconnected_cb, (int)saw_connected, + reported_error.load(), ERR_DNS_RESOLVE); + return -1; +} + +int main() { + int rc = 0; + rc |= test_connect_and_reconnect(); + rc |= test_destroy_mid_resolve(); + rc |= test_resolve_failure_notifies(); + + if (rc == 0) { + printf("\nALL tcpclient_dns_test PASSED\n"); + return 0; + } + printf("\ntcpclient_dns_test FAILED\n"); + return 1; +}