From 2bbb33cd5a9d017981f2eefb9f027e69a4d7ceea Mon Sep 17 00:00:00 2001 From: ithewei Date: Wed, 22 Jul 2026 14:36:55 +0800 Subject: [PATCH 01/17] feat(event): add self-implemented async DNS resolver (hdns) Add a native, non-blocking UDP DNS resolver that runs entirely inside the hloop event loop, replacing blocking getaddrinfo on the async connect path. Self-contained (no thirdparty dep, independent from protocol/dns.c). - event/hdns.{h,c}: async A/AAAA queries, DNS wire codec with name compression, resolv.conf / hosts parsing, Windows GetAdaptersAddresses, 8.8.8.8 fallback, per-attempt timeout + retry + nameserver rotation, numeric-IP fast path, cancelable query handle, TTL-respecting per-loop cache (positive + negative). Callbacks never fire re-entrantly. - event/hloop.c, hevent.h: per-loop resolver ownership, freed in cleanup. - evpp/TcpClient.h: reconnect re-resolves hostnames asynchronously instead of blocking the loop; numeric IPs and initial connect keep the fast path. - examples: hdns_example, hdns_benchmark (getaddrinfo vs async comparison). - unittest: hdns_test (mock nameserver, wire round-trip, cache, cancel, NXDOMAIN), tcpclient_dns_test (hostname connect + async reconnect). - build: Makefile / CMake / Bazel wiring, install header, iphlpapi on win. - docs/cn/hdns.md + README index; mark async DNS done in docs/PLAN.md. --- BUILD.bazel | 1 + Makefile | 13 + Makefile.vars | 1 + cmake/vars.cmake | 1 + docs/PLAN.md | 2 +- docs/cn/README.md | 1 + docs/cn/hdns.md | 154 +++++ event/hdns.c | 956 ++++++++++++++++++++++++++++++++ event/hdns.h | 122 ++++ event/hevent.h | 5 + event/hloop.c | 4 + evpp/TcpClient.h | 62 ++- examples/CMakeLists.txt | 8 + examples/hdns_benchmark.c | 105 ++++ examples/hdns_example.c | 61 ++ scripts/unittest.sh | 6 + unittest/CMakeLists.txt | 15 + unittest/hdns_test.c | 242 ++++++++ unittest/tcpclient_dns_test.cpp | 96 ++++ 19 files changed, 1847 insertions(+), 8 deletions(-) create mode 100644 docs/cn/hdns.md create mode 100644 event/hdns.c create mode 100644 event/hdns.h create mode 100644 examples/hdns_benchmark.c create mode 100644 examples/hdns_example.c create mode 100644 unittest/hdns_test.c create mode 100644 unittest/tcpclient_dns_test.cpp 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..700332cfd 100644 --- a/Makefile +++ b/Makefile @@ -69,6 +69,8 @@ EXAMPLES = hmain_test htimer_test hloop_test pipe_test \ udp_echo_server \ udp_proxy_server \ socks5_proxy_server \ + hdns_example \ + hdns_benchmark \ multi-acceptor-processes \ multi-acceptor-threads \ one-acceptor-multi-workers \ @@ -172,6 +174,12 @@ udp_proxy_server: prepare socks5_proxy_server: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/socks5_proxy_server.c" +hdns_example: prepare + $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/hdns_example.c" + +hdns_benchmark: prepare + $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/hdns_benchmark.c" + multi-acceptor-processes: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/multi-thread/multi-acceptor-processes.c" @@ -305,7 +313,11 @@ 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 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 +329,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/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..455714079 --- /dev/null +++ b/docs/cn/hdns.md @@ -0,0 +1,154 @@ +# 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_options_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_options_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; + +// 查询句柄(不透明,可用于取消) +typedef struct hdns_query_s hdns_query_t; + +// 解析完成回调,在 loop 线程被调用;result 仅在回调期间有效 +typedef void (*hdns_cb)(const hdns_result_t* result, void* userdata); + +// 发起异步解析,绑定到 loop。返回查询句柄,失败返回 NULL。 +hdns_query_t* hdns_resolve(hloop_t* loop, const char* host, + hdns_cb cb, void* userdata); + +// 带选项版本 +hdns_query_t* hdns_resolve_ex(hloop_t* loop, const char* host, + const hdns_options_t* opt, + hdns_cb cb, void* userdata); + +// 取消进行中的查询;返回后回调不会再被调用。只能在 loop 线程调用。 +void hdns_cancel(hdns_query_t* query); + +// 清空该 loop 的 DNS 缓存(如网络切换时)。只能在 loop 线程调用。 +void hdns_clear_cache(hloop_t* loop); +``` + +### 状态码 + +```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(const hdns_result_t* result, void* userdata) { + 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}; + 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/hdns_example.c`,性能对比见 `examples/hdns_benchmark.c`。 + +## 与 connect 路径的集成 + +`hdns` 已接入 C++ 的 `TcpClient`(`evpp/TcpClient.h`): + +- 目标是**数字 IP** 时,走原有同步快速路径,行为不变; +- 目标是**域名**且触发**重连**时,`TcpClient` 会先用 `hdns_resolve` 异步解析(拿到最新 IP,应对 DNS 变化),解析完成后再建立连接——这一步**不再阻塞事件循环**。旧实现在重连时于 loop 线程内调用阻塞的 `getaddrinfo`,会卡住整个 loop。 +- 对象析构或主动 `closesocket()` 时,会自动取消在途的解析查询,避免悬垂回调。 + +> 说明:同步的 `HttpClient` / `requests` 请求路径运行在调用方线程(并非在 loop 内),阻塞解析可接受,故该路径维持不变。 + +## 性能说明 + +`examples/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..de657b74d --- /dev/null +++ b/event/hdns.c @@ -0,0 +1,956 @@ +/* + * 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; + 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. +struct hdns_query_s { + hloop_t* loop; + struct hdns_resolver_s* resolver; + char host[HDNS_NAME_MAXLEN]; + hdns_options_t opt; + hdns_cb cb; + void* userdata; + + // 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 + 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* io; // shared UDP socket + struct list_head queries; // in-flight hdns_query_t + struct list_head cache; // hdns_cache_entry_t (LRU-ish by insertion) + int ncache; + 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_query_t* q); +static void hdns__finish(hdns_query_t* q, int status); + +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; + + // create a non-blocking UDP socket bound to nothing (ephemeral). + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) { + HV_FREE(r); + return NULL; + } + hio_t* io = hio_get(loop, fd); + if (!io) { + closesocket(fd); + HV_FREE(r); + 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); + r->io = 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_query_t* q = list_entry(node, hdns_query_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->io is 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. +static void hdns__deliver(hdns_query_t* q, int status) { + hdns_result_t result; + 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)); + } + hdns_cb cb = q->cb; + void* ud = q->userdata; + + // detach + free before callback so cb may safely destroy related objects + 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); + + if (cb) cb(&result, ud); +} + +// Sort accumulated addresses: IPv4 first (stable), matching getaddrinfo-ish order. +static void hdns__sort_v4_first(hdns_query_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_query_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_query_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_query_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); + + 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(r->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_query_t* q = (hdns_query_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_query_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]; + + // find the query + sub-query for this txid + struct list_node* node; + list_for_each(node, &r->queries) { + hdns_query_t* q = list_entry(node, hdns_query_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; + + // 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_query_t* q = (hdns_query_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_query_t* q = (hdns_query_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_query_t* q) { + // schedule completion on the next loop tick (1ms) so cb never runs + // re-entrantly inside hdns_resolve(). + q->defer_timer = htimer_add(q->loop, hdns__on_defer, 1, 1); + if (q->defer_timer) { + hevent_set_userdata(q->defer_timer, q); + } else { + // extremely unlikely; deliver synchronously as a last resort + hdns__complete(q); + } +} + +hdns_query_t* hdns_resolve_ex(hloop_t* loop, const char* host, + const hdns_options_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_query_t* q; + HV_ALLOC_SIZEOF(q); + if (!q) return NULL; + q->loop = loop; + q->resolver = r; + strncpy(q->host, host, sizeof(q->host) - 1); + q->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); + + // 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. + uint16_t base = (uint16_t)(hv_rand(1, 0xFFFF)); + 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); + } + q->defer_timer = htimer_add(loop, hdns__on_start, 1, 1); + if (q->defer_timer) { + hevent_set_userdata(q->defer_timer, q); + } else { + // extremely unlikely; start synchronously as a last resort + hdns__send_queries(q); + } + return q; +} + +hdns_query_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_query_t* q) { + if (!q) 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..f2db487f6 --- /dev/null +++ b/event/hdns.h @@ -0,0 +1,122 @@ +#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 and CNAME chain following. + * - TTL-respecting per-loop cache (positive + negative). + * - Cancelable query handle. + * + * @see examples/hdns_example.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_options_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_options_s() { + family = HDNS_QUERY_BOTH; + timeout_ms = HDNS_DEFAULT_TIMEOUT_MS; + retries = HDNS_DEFAULT_RETRIES; + use_cache = 1; + nameserver = NULL; + } +#endif +} hdns_options_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; + +// opaque, cancelable handle +typedef struct hdns_query_s hdns_query_t; + +/* + * result callback, invoked in the loop thread. + * NOTE: the result pointer is only valid during the callback. + */ +typedef void (*hdns_cb)(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_query_t* hdns_resolve(hloop_t* loop, const char* host, + hdns_cb cb, void* userdata); + +HV_EXPORT hdns_query_t* hdns_resolve_ex(hloop_t* loop, const char* host, + const hdns_options_t* opt, + hdns_cb cb, void* userdata); + +/* + * Cancel an in-flight query. After this returns, @cb will NOT be called and + * @query is invalid. MUST be called from the loop thread. + */ +HV_EXPORT void hdns_cancel(hdns_query_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/TcpClient.h b/evpp/TcpClient.h index fb6391211..eccd726cf 100644 --- a/evpp/TcpClient.h +++ b/evpp/TcpClient.h @@ -4,6 +4,7 @@ #include "hsocket.h" #include "hssl.h" #include "hlog.h" +#include "hdns.h" #include "EventLoopThread.h" #include "Channel.h" @@ -27,10 +28,12 @@ class TcpClientEventLoopTmpl { reconn_setting = NULL; unpack_setting = NULL; reconn_timer_id = INVALID_TIMER_ID; + dns_query = NULL; } virtual ~TcpClientEventLoopTmpl() { cancelReconnectTimer(); + cancelDnsQuery(); HV_FREE(tls_setting); HV_FREE(reconn_setting); HV_FREE(unpack_setting); @@ -101,6 +104,7 @@ class TcpClientEventLoopTmpl { void closesocket() { if (channel && channel->status != SocketChannel::CLOSED) { loop_->runInLoop([this](){ + cancelDnsQuery(); if (channel) { setReconnect(NULL); channel->close(); @@ -110,15 +114,50 @@ class TcpClientEventLoopTmpl { } int startConnect() { + loop_->assertInLoopThread(); + // On reconnect, re-resolve the hostname to pick up DNS changes. + // Resolution runs asynchronously so the event loop is never blocked. + // Numeric IPs and the initial connect keep using the cached remote_addr. + if (reconn_setting && reconn_setting->cur_retry_cnt > 1 && + !remote_host.empty() && !is_ipaddr(remote_host.c_str())) { + return startResolveThenConnect(); + } + return startConnectWithAddr(); + } + + // @internal: resolve remote_host asynchronously, then connect. + int startResolveThenConnect() { + cancelDnsQuery(); + hdns_options_t opt; + opt.family = HDNS_QUERY_BOTH; + if (connect_timeout > 0) opt.timeout_ms = connect_timeout; + dns_query = hdns_resolve_ex(loop_->loop(), remote_host.c_str(), &opt, + &TcpClientEventLoopTmpl::onDnsResolved, this); + if (dns_query == NULL) { + // could not start async resolve; fall back to synchronous path + return startConnectWithAddr(); + } + return 0; + } + + // @internal: hdns callback (runs in loop thread). + static void onDnsResolved(const hdns_result_t* result, void* userdata) { + TcpClientEventLoopTmpl* self = (TcpClientEventLoopTmpl*)userdata; + self->dns_query = NULL; // handle is invalid after the callback + if (result->status == HDNS_STATUS_OK && result->naddrs > 0) { + // adopt the first resolved address, keep the target port + self->remote_addr = result->addrs[0]; + sockaddr_set_port(&self->remote_addr, self->remote_port); + } + // if resolve failed, fall through with the previous remote_addr; the + // connect attempt will fail and drive the normal reconnect path. + self->startConnectWithAddr(); + } + + int startConnectWithAddr() { 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); - } + 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; @@ -283,9 +322,18 @@ class TcpClientEventLoopTmpl { } } + // Cancel any in-flight async DNS query (loop thread only). + void cancelDnsQuery() { + if (dns_query != NULL) { + hdns_cancel(dns_query); + dns_query = NULL; + } + } + private: EventLoopPtr loop_; TimerID reconn_timer_id; + hdns_query_t* dns_query; }; template diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 422e61eb4..ff292c050 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -12,6 +12,8 @@ list(APPEND EXAMPLES udp_echo_server udp_proxy_server socks5_proxy_server + hdns_example + hdns_benchmark multi-acceptor-processes multi-acceptor-threads one-acceptor-multi-workers @@ -60,6 +62,12 @@ 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(hdns_example hdns_example.c) +target_link_libraries(hdns_example ${HV_LIBRARIES}) + +add_executable(hdns_benchmark hdns_benchmark.c) +target_link_libraries(hdns_benchmark ${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/hdns_benchmark.c b/examples/hdns_benchmark.c new file mode 100644 index 000000000..8e04958da --- /dev/null +++ b/examples/hdns_benchmark.c @@ -0,0 +1,105 @@ +/* + * 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(const hdns_result_t* result, void* userdata) { + 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_options_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/examples/hdns_example.c b/examples/hdns_example.c new file mode 100644 index 000000000..8d6a39462 --- /dev/null +++ b/examples/hdns_example.c @@ -0,0 +1,61 @@ +/* + * hdns_example — asynchronous DNS resolve demo. + * + * Usage: hdns_example [domain] [domain2 ...] + * e.g. hdns_example www.example.com github.com localhost 1.1.1.1 + * + * Resolves each domain asynchronously through the event loop (no blocking + * getaddrinfo) 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(const hdns_result_t* result, void* userdata) { + 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/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..40a891244 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -90,6 +90,11 @@ 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}) + # ------redis------ if(WITH_EVPP AND WITH_REDIS) add_executable(redis_protocol_test redis_protocol_test.cpp ../redis/RedisMessage.cpp) @@ -108,6 +113,14 @@ endforeach() list(APPEND REDIS_UNITTEST_TARGETS ${REDIS_LINKED_UNITTEST_TARGETS}) endif() +# ------evpp: async dns in connect path------ +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 +151,7 @@ add_custom_target(unittest DEPENDS ftp sendmail http_router_test + hdns_test ${REDIS_UNITTEST_TARGETS} + ${EVPP_DNS_UNITTEST_TARGETS} ) diff --git a/unittest/hdns_test.c b/unittest/hdns_test.c new file mode 100644 index 000000000..95fd8dbe3 --- /dev/null +++ b/unittest/hdns_test.c @@ -0,0 +1,242 @@ +/* + * 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_options_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(const hdns_result_t* result, void* userdata) { + 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_options_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_query_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(const hdns_result_t* result, void* userdata) { + (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(HLOOP_FLAG_QUIT_WHEN_NO_ACTIVE_EVENTS == 0 ? 0 : 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_options_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_options_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_options_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_options_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_options_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_query_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..3a2f7b879 --- /dev/null +++ b/unittest/tcpclient_dns_test.cpp @@ -0,0 +1,96 @@ +/* + * tcpclient_dns_test — integration test for async DNS in the evpp connect path. + * + * Verifies that TcpClient with a *hostname* target (not a numeric IP) connects + * successfully and that reconnect re-resolves the hostname through the async + * resolver (TcpClientEventLoopTmpl::startResolveThenConnect) without blocking + * the event loop. + * + * Uses "localhost" (resolved via /etc/hosts by hdns) against a local TCP + * server, so it needs no external network. + */ + +#include +#include + +#include "TcpServer.h" +#include "TcpClient.h" +#include "EventLoop.h" +#include "htime.h" + +using namespace hv; + +static std::atomic g_connect_count{0}; +static std::atomic g_reconnected{false}; + +int main() { + // 1. start a TCP echo server on a fixed local port + int port = 40833; + TcpServer srv; + int listenfd = srv.createsocket(port, "127.0.0.1"); + if (listenfd < 0) { + printf("createsocket failed on port %d\n", port); + return 1; + } + printf("server listening on 127.0.0.1:%d\n", port); + srv.onMessage = [](const SocketChannelPtr& ch, Buffer* buf) { + ch->write(buf); // echo + }; + srv.start(); + + // 2. TcpClient targeting "localhost" (a hostname -> exercises DNS path) + auto cli = std::make_shared(); + int connfd = cli->createsocket(port, "localhost"); + if (connfd < 0) { + printf("client createsocket failed\n"); + return 2; + } + + 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 = ++g_connect_count; + printf("connected to %s (count=%d)\n", channel->peeraddr().c_str(), n); + if (n == 1) { + // force a disconnect shortly to trigger reconnect + re-resolve. + // channel->close() is thread-safe and runs in the loop thread. + setTimeout(200, [channel](TimerID) { + printf("closing to trigger reconnect...\n"); + channel->close(); + }); + } else if (n >= 2) { + g_reconnected = true; + } + } else { + printf("disconnected\n"); + } + }; + cli->start(); + + // 3. run until reconnect confirmed or timeout + uint64_t start = gettick_ms(); + while (!g_reconnected && gettick_ms() - start < 8000) { + hv_msleep(50); + } + + // stop reconnect first (thread-safe), then let the client shut down. + cli->setReconnect(NULL); + cli->stop(true); + srv.stop(); + hv_msleep(200); + + if (g_connect_count >= 2 && g_reconnected) { + printf("\nPASS: hostname connect + async re-resolve reconnect worked " + "(connects=%d)\n", g_connect_count.load()); + return 0; + } + printf("\nFAIL: connects=%d reconnected=%d\n", + g_connect_count.load(), (int)g_reconnected); + return 3; +} From 1592b3942ae78966ba75f931bf43a30461008c84 Mon Sep 17 00:00:00 2001 From: ithewei Date: Wed, 22 Jul 2026 17:17:02 +0800 Subject: [PATCH 02/17] feat(http): use async DNS (hdns) in AsyncHttpClient AsyncHttpClient::doTask ran in the loop thread but resolved hostnames with blocking getaddrinfo (via sockaddr_set_ipport), stalling the event loop. - Split doTask: numeric IP / UDS keep the synchronous fast path; hostnames are resolved asynchronously via hdns_resolve, then continue in doTaskWithAddr from the callback. The loop is never blocked. - Track in-flight DnsContext handles; cancel them and free contexts in the destructor to avoid leaks / dangling callbacks. - unittest/asynchttp_dns_test: async HTTP request to a hostname URL (localhost) against a local server, resolved through hdns. - build wiring (Makefile / CMake / unittest.sh); update docs/cn/hdns.md. --- Makefile | 9 +++- docs/cn/hdns.md | 10 ++++- http/client/AsyncHttpClient.cpp | 80 ++++++++++++++++++++++++++++++--- http/client/AsyncHttpClient.h | 22 +++++++++ scripts/unittest.sh | 3 ++ unittest/CMakeLists.txt | 8 ++++ unittest/asynchttp_dns_test.cpp | 76 +++++++++++++++++++++++++++++++ 7 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 unittest/asynchttp_dns_test.cpp diff --git a/Makefile b/Makefile index 700332cfd..0ffbc6394 100644 --- a/Makefile +++ b/Makefile @@ -318,6 +318,13 @@ unittest: prepare 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_HTTP), yes) +ifeq ($(WITH_HTTP_CLIENT), yes) +ifeq ($(WITH_HTTP_SERVER), yes) + $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -o bin/asynchttp_dns_test unittest/asynchttp_dns_test.cpp -Llib -lhv -pthread +endif +endif +endif 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 @@ -329,7 +336,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/tcpclient_dns_test bin/asynchttp_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/docs/cn/hdns.md b/docs/cn/hdns.md index 455714079..2ebd7e445 100644 --- a/docs/cn/hdns.md +++ b/docs/cn/hdns.md @@ -139,12 +139,20 @@ int main() { ## 与 connect 路径的集成 -`hdns` 已接入 C++ 的 `TcpClient`(`evpp/TcpClient.h`): +`hdns` 已接入 C++ 的 `TcpClient`(`evpp/TcpClient.h`)与异步 HTTP 客户端 `AsyncHttpClient`(`http/client/AsyncHttpClient.cpp`): + +### TcpClient - 目标是**数字 IP** 时,走原有同步快速路径,行为不变; - 目标是**域名**且触发**重连**时,`TcpClient` 会先用 `hdns_resolve` 异步解析(拿到最新 IP,应对 DNS 变化),解析完成后再建立连接——这一步**不再阻塞事件循环**。旧实现在重连时于 loop 线程内调用阻塞的 `getaddrinfo`,会卡住整个 loop。 - 对象析构或主动 `closesocket()` 时,会自动取消在途的解析查询,避免悬垂回调。 +### AsyncHttpClient + +- 请求 URL 里是**数字 IP**(或 Unix Domain Socket)时,走原有同步快速路径; +- 请求 URL 里是**域名**时,`doTask` 会先用 `hdns_resolve` 异步解析,回调里再建立连接、发送请求——同样**不阻塞 loop**。旧实现直接在 loop 线程里对域名做阻塞 `getaddrinfo`。 +- 客户端析构时会取消所有在途解析并释放其上下文,避免泄漏与悬垂回调。 + > 说明:同步的 `HttpClient` / `requests` 请求路径运行在调用方线程(并非在 loop 内),阻塞解析可接受,故该路径维持不变。 ## 性能说明 diff --git a/http/client/AsyncHttpClient.cpp b/http/client/AsyncHttpClient.cpp index f20d7cc8c..3b148b421 100644 --- a/http/client/AsyncHttpClient.cpp +++ b/http/client/AsyncHttpClient.cpp @@ -34,14 +34,84 @@ 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 hdns so the event loop + // is never blocked by getaddrinfo. + 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); + } + + // async resolve hostname, then continue in onDnsResolved + DnsContext* dctx = new DnsContext(); + dctx->client = this; + dctx->task = task; + dctx->query = NULL; + dns_queries.insert(dctx); + + hdns_options_t opt; + if (req->connect_timeout > 0) opt.timeout_ms = req->connect_timeout * 1000; + dctx->query = hdns_resolve_ex(EventLoopThread::hloop(), host, &opt, + &AsyncHttpClient::onDnsResolved, dctx); + if (dctx->query == NULL) { + dns_queries.erase(dctx); + delete dctx; + hloge("hdns_resolve failed for host %s", host); return -20; } + return 0; +} + +// hdns callback (runs in loop thread). +void AsyncHttpClient::onDnsResolved(const hdns_result_t* result, void* userdata) { + DnsContext* dctx = (DnsContext*)userdata; + AsyncHttpClient* self = dctx->client; + HttpClientTaskPtr task = dctx->task; + // handle is invalid after this callback; drop the tracking context. + self->dns_queries.erase(dctx); + delete dctx; + + if (result->status != HDNS_STATUS_OK || result->naddrs <= 0) { + hloge("resolve host %s failed, status=%d", result->host, result->status); + if (task->cb) task->cb(NULL); + return; + } + + // adopt the first resolved address and set the target port. + sockaddr_u peeraddr = result->addrs[0]; + sockaddr_set_port(&peeraddr, task->req->port); + + int err = self->doTaskWithAddr(task, &peeraddr); + if (err != 0 && task->cb) { + task->cb(NULL); + } +} + +// 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..e9c0cde41 100644 --- a/http/client/AsyncHttpClient.h +++ b/http/client/AsyncHttpClient.h @@ -3,9 +3,11 @@ #include #include +#include #include "EventLoopThread.h" #include "Channel.h" +#include "hdns.h" #include "HttpMessage.h" #include "HttpParser.h" @@ -111,6 +113,13 @@ class HV_EXPORT AsyncHttpClient : private EventLoopThread { } ~AsyncHttpClient() { EventLoopThread::stop(true); + // cancel any in-flight async DNS queries and free their contexts. + // Safe here: the loop thread has stopped, so no callback can race. + for (auto* dctx : dns_queries) { + if (dctx->query) hdns_cancel(dctx->query); + delete dctx; + } + dns_queries.clear(); } // thread-safe @@ -129,6 +138,17 @@ class HV_EXPORT AsyncHttpClient : private EventLoopThread { } int doTask(const HttpClientTaskPtr& task); + // async DNS resolve context for one task (heap-allocated, tracked so it can + // be cancelled/freed on teardown). + struct DnsContext { + AsyncHttpClient* client; + HttpClientTaskPtr task; + hdns_query_t* query; + }; + // @internal: continue doTask after the peer address is known. + int doTaskWithAddr(const HttpClientTaskPtr& task, const sockaddr_u* peeraddr); + static void onDnsResolved(const hdns_result_t* result, void* userdata); + static int sendRequest(const SocketChannelPtr& channel); // channel @@ -157,6 +177,8 @@ class HV_EXPORT AsyncHttpClient : private EventLoopThread { std::map channels; // peeraddr => ConnPool std::map> conn_pools; + // in-flight async DNS resolve contexts (for cancel/free on teardown) + std::set dns_queries; }; } diff --git a/scripts/unittest.sh b/scripts/unittest.sh index f32edee13..82456330f 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -38,6 +38,9 @@ fi if [ -x bin/tcpclient_dns_test ]; then bin/tcpclient_dns_test fi +if [ -x bin/asynchttp_dns_test ]; then + bin/asynchttp_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 40a891244..607dae6e4 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -121,6 +121,14 @@ target_link_libraries(tcpclient_dns_test ${HV_LIBRARIES}) set(EVPP_DNS_UNITTEST_TARGETS tcpclient_dns_test) endif() +# ------http: async dns in AsyncHttpClient------ +if(WITH_EVPP AND WITH_HTTP AND WITH_HTTP_CLIENT AND WITH_HTTP_SERVER) +add_executable(asynchttp_dns_test asynchttp_dns_test.cpp) +target_include_directories(asynchttp_dns_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/client ../http/server) +target_link_libraries(asynchttp_dns_test ${HV_LIBRARIES}) +list(APPEND EVPP_DNS_UNITTEST_TARGETS asynchttp_dns_test) +endif() + if(UNIX) add_executable(webbench webbench.c) endif() diff --git a/unittest/asynchttp_dns_test.cpp b/unittest/asynchttp_dns_test.cpp new file mode 100644 index 000000000..66edc1cbe --- /dev/null +++ b/unittest/asynchttp_dns_test.cpp @@ -0,0 +1,76 @@ +/* + * asynchttp_dns_test — integration test for async DNS in AsyncHttpClient. + * + * Verifies that an async HTTP request to a *hostname* URL (not a numeric IP) + * resolves the hostname through hdns (async, non-blocking) and completes. + * + * Uses "http://localhost:PORT/ping" against a local HttpServer, so it needs no + * external network (localhost resolves via /etc/hosts inside hdns). + */ + +#include +#include + +#include "HttpServer.h" +#include "HttpClient.h" // http_client_send_async +#include "htime.h" + +using namespace hv; + +int main() { + // 1. start a local HTTP server on a fixed port + int port = 10880; + HttpService router; + router.GET("/ping", [](HttpRequest* req, HttpResponse* resp) { + return resp->String("pong"); + }); + + HttpServer server; + server.registerHttpService(&router); + server.setPort(port); + server.setThreadNum(1); + if (server.start() != 0) { + printf("server start failed on port %d\n", port); + return 1; + } + printf("http server started on localhost:%d\n", port); + hv_msleep(200); + + // 2. issue an async HTTP request to the *hostname* URL + std::atomic done{false}; + std::atomic status_code{0}; + std::string body; + + auto req = std::make_shared(); + char url[128]; + snprintf(url, sizeof(url), "http://localhost:%d/ping", port); + req->url = url; // hostname "localhost" -> exercises async DNS + req->method = HTTP_GET; + req->timeout = 5; + + http_client_send_async(req, [&](const HttpResponsePtr& resp) { + if (resp) { + status_code = resp->status_code; + body = resp->body; + } + done = true; + }); + + // 3. wait for completion + uint64_t start = gettick_ms(); + while (!done && gettick_ms() - start < 6000) { + hv_msleep(20); + } + + server.stop(); + hv_msleep(100); + + if (done && status_code == 200 && body == "pong") { + printf("\nPASS: async HTTP request to hostname resolved via hdns " + "(status=%d body=%s)\n", status_code.load(), body.c_str()); + return 0; + } + printf("\nFAIL: done=%d status=%d body=%s\n", + (int)done, status_code.load(), body.c_str()); + return 2; +} From 3330411d44a4fbd9a15f940a792e3fbf96d85cfd Mon Sep 17 00:00:00 2001 From: ithewei Date: Wed, 22 Jul 2026 18:52:30 +0800 Subject: [PATCH 03/17] refactor(evpp): make async DNS generic for all TcpClientTmpl subclasses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move async hostname resolution fully into the TcpClientEventLoopTmpl base so every client that inherits from TcpClientTmpl (TcpClient, WebSocketClient, and any future XXXClient) gets non-blocking DNS for free — no per-client onDnsResolved glue. - startConnect() now async-resolves hostnames on BOTH first connect and every reconnect (previously reconnect-only), superseding the special case. - createsocket(port, host) no longer blocks on getaddrinfo for hostnames: it records host/port and defers resolution to startConnect(); numeric IPs / UDS keep the synchronous fast path and the connfd contract. - onDnsResolved: on resolve failure with no usable prior address, drive the normal failure/reconnect callback path instead of connecting to a zero addr. - WebSocketClient needs zero changes; it inherits the behavior. Its previous blocking createsocket() in open() (also hit on redirect in the loop thread) is now non-blocking via the base. - unittest/websocket_dns_test: ws://localhost connect + echo, proving the generic base handles WebSocket with no WS-specific DNS code. - build wiring (Makefile / CMake / unittest.sh); update docs/cn/hdns.md. --- Makefile | 3 +- docs/cn/hdns.md | 14 ++++-- evpp/TcpClient.h | 55 ++++++++++++++++----- scripts/unittest.sh | 3 ++ unittest/CMakeLists.txt | 6 +++ unittest/websocket_dns_test.cpp | 84 +++++++++++++++++++++++++++++++++ 6 files changed, 147 insertions(+), 18 deletions(-) create mode 100644 unittest/websocket_dns_test.cpp diff --git a/Makefile b/Makefile index 0ffbc6394..9d47c7160 100644 --- a/Makefile +++ b/Makefile @@ -322,6 +322,7 @@ ifeq ($(WITH_HTTP), yes) ifeq ($(WITH_HTTP_CLIENT), yes) ifeq ($(WITH_HTTP_SERVER), yes) $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -o bin/asynchttp_dns_test unittest/asynchttp_dns_test.cpp -Llib -lhv -pthread + $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Iutil -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -o bin/websocket_dns_test unittest/websocket_dns_test.cpp -Llib -lhv -pthread endif endif endif @@ -336,7 +337,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 bin/asynchttp_dns_test + $(RM) bin/tcpclient_dns_test bin/asynchttp_dns_test bin/websocket_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/docs/cn/hdns.md b/docs/cn/hdns.md index 2ebd7e445..81574c91e 100644 --- a/docs/cn/hdns.md +++ b/docs/cn/hdns.md @@ -139,16 +139,22 @@ int main() { ## 与 connect 路径的集成 -`hdns` 已接入 C++ 的 `TcpClient`(`evpp/TcpClient.h`)与异步 HTTP 客户端 `AsyncHttpClient`(`http/client/AsyncHttpClient.cpp`): +`hdns` 已接入 C++ 客户端的 connect 路径。**异步解析逻辑统一沉淀在基类 `TcpClientEventLoopTmpl`(`evpp/TcpClient.h`)里**,因此所有继承自 `TcpClientTmpl` 的客户端(`TcpClient`、`WebSocketClient`,以及未来任何 `XXXClient`)都**自动获得**异步 DNS,无需各自编写胶水代码。 -### TcpClient +### TcpClientTmpl 派生类(TcpClient / WebSocketClient / ...) -- 目标是**数字 IP** 时,走原有同步快速路径,行为不变; -- 目标是**域名**且触发**重连**时,`TcpClient` 会先用 `hdns_resolve` 异步解析(拿到最新 IP,应对 DNS 变化),解析完成后再建立连接——这一步**不再阻塞事件循环**。旧实现在重连时于 loop 线程内调用阻塞的 `getaddrinfo`,会卡住整个 loop。 +- 目标是**数字 IP**(或 Unix Domain Socket)时,`createsocket` 走原有同步快速路径立即建 socket,行为不变; +- 目标是**域名**时,`createsocket` 只记录 host/port 并**延迟解析**(不阻塞);`startConnect()`(首次连接与每次重连都会调用)先用 `hdns_resolve` 异步解析,拿到地址后再在 `startConnectWithAddr()` 建 socket、connect。整个过程**不阻塞事件循环**。旧实现在 loop 线程里调用阻塞的 `getaddrinfo`,会卡住整个 loop。 +- 每次连接/重连都会重新解析,自动应对 DNS 变化; - 对象析构或主动 `closesocket()` 时,会自动取消在途的解析查询,避免悬垂回调。 +- 若解析失败且无可用历史地址,会走正常的失败/重连回调。 + +> 新增客户端零成本:只要继承 `TcpClientTmpl` 并用 `createsocket(port, host)` + `start()`,就自动拥有异步 DNS,不需要实现任何 `onDnsResolved` 之类的回调。 ### AsyncHttpClient +`AsyncHttpClient` 不继承 `TcpClientTmpl`(自带连接池等逻辑),单独接入: + - 请求 URL 里是**数字 IP**(或 Unix Domain Socket)时,走原有同步快速路径; - 请求 URL 里是**域名**时,`doTask` 会先用 `hdns_resolve` 异步解析,回调里再建立连接、发送请求——同样**不阻塞 loop**。旧实现直接在 loop 线程里对域名做阻塞 `getaddrinfo`。 - 客户端析构时会取消所有在途解析并释放其上下文,避免泄漏与悬垂回调。 diff --git a/evpp/TcpClient.h b/evpp/TcpClient.h index eccd726cf..2751f8f75 100644 --- a/evpp/TcpClient.h +++ b/evpp/TcpClient.h @@ -53,15 +53,25 @@ class TcpClientEventLoopTmpl { // NOTE: By default, not bind local port. If necessary, you can call bind() after createsocket(). // @retval >=0 connfd, <0 error + // NOTE: If remote_host is a hostname (not a numeric IP), resolution is + // deferred and done asynchronously in startConnect() so the event loop is + // never blocked by getaddrinfo. In that case no socket/connfd exists yet + // and this returns 0; the socket is created once the address is resolved. 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, preserving the connfd contract. + 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 to async resolution in startConnect() + return 0; } int createsocket(struct sockaddr* remote_addr) { @@ -115,11 +125,13 @@ class TcpClientEventLoopTmpl { int startConnect() { loop_->assertInLoopThread(); - // On reconnect, re-resolve the hostname to pick up DNS changes. - // Resolution runs asynchronously so the event loop is never blocked. - // Numeric IPs and the initial connect keep using the cached remote_addr. - if (reconn_setting && reconn_setting->cur_retry_cnt > 1 && - !remote_host.empty() && !is_ipaddr(remote_host.c_str())) { + // 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. + if (!remote_host.empty() && !is_ipaddr(remote_host.c_str())) { return startResolveThenConnect(); } return startConnectWithAddr(); @@ -148,12 +160,29 @@ class TcpClientEventLoopTmpl { // adopt the first resolved address, keep the target port self->remote_addr = result->addrs[0]; sockaddr_set_port(&self->remote_addr, self->remote_port); + } else if (self->remote_addr.sa.sa_family == 0) { + // resolve failed and we have no previously-resolved address to fall + // back to. Report failure and drive reconnect (if enabled). + hloge("resolve %s failed, status=%d", self->remote_host.c_str(), result->status); + self->onResolveFailed(); + return; } - // if resolve failed, fall through with the previous remote_addr; the - // connect attempt will fail and drive the normal reconnect path. + // else: resolve failed but keep the previous remote_addr; the connect + // attempt will fail and drive the normal reconnect path. self->startConnectWithAddr(); } + // @internal: DNS resolution failed with no usable address. + void onResolveFailed() { + if (onConnection && channel) { + // channel is not connected; notify disconnect-style callback. + onConnection(channel); + } + if (reconn_setting) { + startReconnect(); + } + } + int startConnectWithAddr() { loop_->assertInLoopThread(); if (channel == NULL || channel->isClosed()) { diff --git a/scripts/unittest.sh b/scripts/unittest.sh index 82456330f..9b76bd9ad 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -41,6 +41,9 @@ fi if [ -x bin/asynchttp_dns_test ]; then bin/asynchttp_dns_test fi +if [ -x bin/websocket_dns_test ]; then + bin/websocket_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 607dae6e4..cc5e1cf2f 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -127,6 +127,12 @@ add_executable(asynchttp_dns_test asynchttp_dns_test.cpp) target_include_directories(asynchttp_dns_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/client ../http/server) target_link_libraries(asynchttp_dns_test ${HV_LIBRARIES}) list(APPEND EVPP_DNS_UNITTEST_TARGETS asynchttp_dns_test) + +# WebSocketClient inherits async DNS from the TcpClient base (no WS-specific glue) +add_executable(websocket_dns_test websocket_dns_test.cpp) +target_include_directories(websocket_dns_test PRIVATE .. ../base ../ssl ../event ../util ../cpputil ../evpp ../http ../http/client ../http/server) +target_link_libraries(websocket_dns_test ${HV_LIBRARIES}) +list(APPEND EVPP_DNS_UNITTEST_TARGETS websocket_dns_test) endif() if(UNIX) diff --git a/unittest/websocket_dns_test.cpp b/unittest/websocket_dns_test.cpp new file mode 100644 index 000000000..0441b040a --- /dev/null +++ b/unittest/websocket_dns_test.cpp @@ -0,0 +1,84 @@ +/* + * websocket_dns_test — integration test proving async DNS works generically + * for WebSocketClient with NO WebSocket-specific DNS glue. + * + * WebSocketClient extends TcpClientTmpl, so it inherits the async hostname + * resolution baked into TcpClientEventLoopTmpl::startConnect(). Connecting to a + * hostname URL (ws://localhost:PORT/) must resolve via hdns without blocking + * the loop, then complete the WS handshake. + * + * Uses "localhost" against a local WebSocketServer, so it needs no external + * network (localhost resolves via /etc/hosts inside hdns). + */ + +#include +#include + +#include "WebSocketServer.h" +#include "WebSocketClient.h" +#include "htime.h" + +using namespace hv; + +int main() { + int port = 19999; + + // 1. start a local WebSocket echo server + WebSocketService ws; + ws.onmessage = [](const WebSocketChannelPtr& channel, const std::string& msg) { + channel->send(msg); // echo + }; + + WebSocketServer server; + server.port = port; + server.registerWebSocketService(&ws); + if (server.start() != 0) { + printf("websocket server start failed on port %d\n", port); + return 1; + } + printf("websocket server started on localhost:%d\n", port); + hv_msleep(200); + + // 2. connect via a *hostname* URL (exercises generic async DNS in the base) + std::atomic opened{false}; + std::atomic got_echo{false}; + std::string received; + + WebSocketClient cli; + cli.onopen = [&]() { + printf("ws onopen\n"); + opened = true; + cli.send("hello"); + }; + cli.onmessage = [&](const std::string& msg) { + printf("ws onmessage: %s\n", msg.c_str()); + received = msg; + got_echo = true; + }; + cli.onclose = [&]() { + printf("ws onclose\n"); + }; + + char url[128]; + snprintf(url, sizeof(url), "ws://localhost:%d/", port); + cli.open(url); + + // 3. wait for echo or timeout + uint64_t start = gettick_ms(); + while (!got_echo && gettick_ms() - start < 6000) { + hv_msleep(20); + } + + cli.close(); + server.stop(); + hv_msleep(100); + + if (opened && got_echo && received == "hello") { + printf("\nPASS: websocket connect to hostname resolved via hdns " + "(echo=%s)\n", received.c_str()); + return 0; + } + printf("\nFAIL: opened=%d got_echo=%d received=%s\n", + (int)opened, (int)got_echo, received.c_str()); + return 2; +} From caf14a40d1d1252c0bb7335c3efd73bb78959fc1 Mon Sep 17 00:00:00 2001 From: ithewei Date: Wed, 22 Jul 2026 19:57:33 +0800 Subject: [PATCH 04/17] docs(evpp): clarify createsocket(port, host) return-value contract The return value differs between numeric-IP and hostname targets, which looked inconsistent. Document the three-state contract explicitly (matching the >0/=0/<0 convention already used by examples/protorpc): >0 connfd - socket created immediately (numeric IP, or UDS when port < 0) =0 pending - hostname; socket creation deferred until async DNS resolves (not an error; real fd available later via channel->fd()) <0 error Also explain why the =0 case is unavoidable: a socket's address family (AF_INET vs AF_INET6) must be known before socket(), but for a hostname the family is only known after DNS returns an A/AAAA record, so the socket is created post-resolution in startConnectWithAddr() -- same order as the standard getaddrinfo connect loop. Comment-only change; no behavior change. --- evpp/TcpClient.h | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/evpp/TcpClient.h b/evpp/TcpClient.h index 2751f8f75..649d6c6cf 100644 --- a/evpp/TcpClient.h +++ b/evpp/TcpClient.h @@ -52,17 +52,29 @@ class TcpClientEventLoopTmpl { } // NOTE: By default, not bind local port. If necessary, you can call bind() after createsocket(). - // @retval >=0 connfd, <0 error - // NOTE: If remote_host is a hostname (not a numeric IP), resolution is - // deferred and done asynchronously in startConnect() so the event loop is - // never blocked by getaddrinfo. In that case no socket/connfd exists yet - // and this returns 0; the socket is created once the address is resolved. + // + // @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(). This matches the >0/=0/<0 convention used by + // other evpp clients (see examples/protorpc). + // @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") { this->remote_host = remote_host; this->remote_port = remote_port; memset(&remote_addr, 0, sizeof(remote_addr)); // numeric IP (or UDS: port < 0) -> resolve now (non-blocking) and - // create the socket immediately, preserving the connfd contract. + // 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) { @@ -70,7 +82,8 @@ class TcpClientEventLoopTmpl { } return createsocket(&remote_addr.sa); } - // hostname -> defer to async resolution in startConnect() + // hostname -> defer socket creation to async resolution in + // startConnect(); return 0 (pending), not an error. return 0; } From 51ca4812fb3ee37a0e3de9bbf12ae247c99a6b0c Mon Sep 17 00:00:00 2001 From: ithewei Date: Wed, 22 Jul 2026 20:38:30 +0800 Subject: [PATCH 05/17] fix(hdns): address review findings (UAF, IPv6 nameserver, txid, QR bit) - AsyncHttpClient dtor use-after-free: an owned loop uses HLOOP_FLAG_AUTO_FREE, so EventLoopThread::stop() runs hloop_free() -> hdns_resolver_free(), which already frees every in-flight hdns_query_t. The dtor then called hdns_cancel() on those freed pointers. Now the dtor only frees its own DnsContext shells and never touches the (already-freed) query handles. - IPv6 nameserver transport: the resolver used a single AF_INET UDP socket, so sendto() to an IPv6 nameserver (resolv.conf "nameserver ::1", Windows IPv6 DNS) always failed and timed out. Create the send/recv socket lazily per nameserver family (io4/io6) and pick the matching one per query. - Concurrent-query txid collisions: txids were random (hv_rand), so two in-flight queries could share a txid and get responses demuxed to the wrong query. Use a per-resolver monotonic counter (seeded randomly), stepping by 2 since each query uses base (A) and base+1 (AAAA). - Response validation: reject packets without the QR (response) bit set, in addition to the existing txid + rcode checks. All DNS unit/integration tests pass (hdns_test, tcpclient/asynchttp/websocket _dns_test) under both Makefile and CMake; real-network resolve verified. --- event/hdns.c | 64 +++++++++++++++++++++++------------ http/client/AsyncHttpClient.h | 11 ++++-- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/event/hdns.c b/event/hdns.c index de657b74d..dc80a54cd 100644 --- a/event/hdns.c +++ b/event/hdns.c @@ -163,6 +163,8 @@ static int hdns__parse_response(const uint8_t* buf, int buflen, uint16_t expect_ 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 @@ -482,10 +484,12 @@ struct hdns_query_s { typedef struct hdns_resolver_s { hloop_t* loop; - hio_t* io; // shared UDP socket + 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_query_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 uint8_t sndbuf[HDNS_UDP_BUFSIZE]; } hdns_resolver_t; @@ -496,6 +500,27 @@ static void hdns__on_defer(htimer_t* timer); static void hdns__send_queries(hdns_query_t* q); static void hdns__finish(hdns_query_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; @@ -506,24 +531,9 @@ static hdns_resolver_t* hdns__resolver_get(hloop_t* loop) { list_init(&r->queries); list_init(&r->cache); r->ncache = 0; - - // create a non-blocking UDP socket bound to nothing (ephemeral). - int fd = socket(AF_INET, SOCK_DGRAM, 0); - if (fd < 0) { - HV_FREE(r); - return NULL; - } - hio_t* io = hio_get(loop, fd); - if (!io) { - closesocket(fd); - HV_FREE(r); - 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); - r->io = io; + // 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; @@ -549,7 +559,7 @@ void hdns_resolver_free(hloop_t* loop) { list_del(&e->node); HV_FREE(e); } - // NOTE: r->io is owned by the loop and freed by hloop_cleanup's io sweep. + // NOTE: r->io4/io6 are owned by the loop and freed by hloop_cleanup's io sweep. HV_FREE(r); } @@ -719,6 +729,13 @@ static void hdns__send_queries(hdns_query_t* q) { } 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; + } + 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, @@ -729,7 +746,7 @@ static void hdns__send_queries(hdns_query_t* q) { continue; } q->sub[i].active = 1; - hio_sendto(r->io, r->sndbuf, len, &ns.sa); + hio_sendto(io, r->sndbuf, len, &ns.sa); } if (hdns__all_done(q)) { @@ -923,7 +940,10 @@ hdns_query_t* hdns_resolve_ex(hloop_t* loop, const char* host, // 3) network query: assign sub-queries + txids, and start on the next loop // tick so this function returns the handle before any send/callback. - uint16_t base = (uint16_t)(hv_rand(1, 0xFFFF)); + // Use a per-resolver monotonic counter (not random) so concurrent + // queries never share a txid and responses demux unambiguously. + 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; diff --git a/http/client/AsyncHttpClient.h b/http/client/AsyncHttpClient.h index e9c0cde41..048409dcc 100644 --- a/http/client/AsyncHttpClient.h +++ b/http/client/AsyncHttpClient.h @@ -112,11 +112,16 @@ class HV_EXPORT AsyncHttpClient : private EventLoopThread { } } ~AsyncHttpClient() { + // Stop (and, for an owned loop, free) the event loop first. An owned + // loop uses HLOOP_FLAG_AUTO_FREE, so joining the loop thread runs + // hloop_free() -> hdns_resolver_free(), which cancels and frees every + // in-flight hdns_query_t. After stop() the loop thread is joined, so no + // resolve callback can race with teardown. EventLoopThread::stop(true); - // cancel any in-flight async DNS queries and free their contexts. - // Safe here: the loop thread has stopped, so no callback can race. + // The resolver already freed the hdns_query_t objects during loop + // teardown, so DO NOT call hdns_cancel() here (that would be a + // use-after-free). Just free our own per-request context shells. for (auto* dctx : dns_queries) { - if (dctx->query) hdns_cancel(dctx->query); delete dctx; } dns_queries.clear(); From 23db90319b2be8c2efb1e69cd6ae11dd77f3ba6f Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 23 Jul 2026 12:58:12 +0800 Subject: [PATCH 06/17] refactor(hdns): id-based handles via EventLoop to eliminate UAF risk The C handle was a raw hdns_query_t* held by clients, which was fragile: when the loop tore down (hloop_free -> hdns_resolver_free) it freed in-flight queries while a client still held the pointer, and a resolve callback could fire into a destroyed client -- both use-after-free / double-free hazards. Redesign the handle ownership to mirror how EventLoop already handles timers (TimerID -> htimer_t), which is the codebase's established UAF-proof pattern: - event/hdns.{h,c}: rename hdns_query_t -> hdns_t and make struct hdns_s a subclass of hevent_t (HEVENT_FIELDS, like htimer_s/hio_s), so it carries an event_id and can use hevent_set_id/hevent_id. The C API keeps returning a hdns_t* (same raw-pointer contract as htimer_t) and hdns_cancel(hdns_t*) frees immediately. hdns_cb now passes the hdns_t* handle to the callback (like htimer_cb/hio_cb) so a completion handler can recover its id. - evpp/Event.h + EventLoop.h: add DnsID (uint64) and a DnsID -> DnsQuery map on EventLoop, with resolveDns()/cancelDns() mirroring setTimer()/killTimer(). The DnsID rides in the query's event_id; onDnsResolved erases the map entry before invoking the C++ callback. A stale DnsID simply misses the map, so cancelDns() is a safe no-op -- clients hold an id, never a raw pointer. - evpp/TcpClient.h: hold a DnsID; resolve/cancel via EventLoop. Removes the per-class static onDnsResolved glue (now a lambda). - http/client/AsyncHttpClient.{h,cpp}: use EventLoop::resolveDns; drop the DnsContext tracking set and its destructor cleanup (loop owns lifetime now). - unittest/dns_lifetime_test: regression that destroys a TcpClient while a hostname resolve is in flight (the exact old UAF scenario); wired into Makefile / CMake / unittest.sh. - update C tests/examples to the new hdns_cb signature; refresh docs/cn/hdns.md. All DNS tests pass (hdns_test, tcpclient/asynchttp/websocket/dns_lifetime) under Makefile and CMake; real-network resolve verified. NOTE: AddressSanitizer could not run in this environment (even a trivial ASan binary hangs at startup), so UAF-freedom is argued by the id-map design + the non-ASan lifetime test. --- Makefile | 3 +- docs/cn/hdns.md | 56 ++++++++++++------ event/hdns.c | 101 +++++++++++++++++++------------- event/hdns.h | 37 ++++++++---- evpp/Event.h | 25 ++++++++ evpp/EventLoop.h | 52 ++++++++++++++++ evpp/TcpClient.h | 54 ++++++++--------- examples/hdns_benchmark.c | 3 +- examples/hdns_example.c | 3 +- http/client/AsyncHttpClient.cpp | 58 ++++++------------ http/client/AsyncHttpClient.h | 27 +-------- scripts/unittest.sh | 3 + unittest/CMakeLists.txt | 6 ++ unittest/dns_lifetime_test.cpp | 49 ++++++++++++++++ unittest/hdns_test.c | 11 ++-- 15 files changed, 318 insertions(+), 170 deletions(-) create mode 100644 unittest/dns_lifetime_test.cpp diff --git a/Makefile b/Makefile index 9d47c7160..b19cedde4 100644 --- a/Makefile +++ b/Makefile @@ -318,6 +318,7 @@ unittest: prepare 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 + $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -o bin/dns_lifetime_test unittest/dns_lifetime_test.cpp -Llib -lhv -pthread ifeq ($(WITH_HTTP), yes) ifeq ($(WITH_HTTP_CLIENT), yes) ifeq ($(WITH_HTTP_SERVER), yes) @@ -337,7 +338,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 bin/asynchttp_dns_test bin/websocket_dns_test + $(RM) bin/tcpclient_dns_test bin/dns_lifetime_test bin/asynchttp_dns_test bin/websocket_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/docs/cn/hdns.md b/docs/cn/hdns.md index 81574c91e..41800cd95 100644 --- a/docs/cn/hdns.md +++ b/docs/cn/hdns.md @@ -58,28 +58,34 @@ typedef struct hdns_result_s { sockaddr_u addrs[HDNS_MAX_ADDRS]; // A/AAAA 合并结果(IPv4 在前),端口为 0 } hdns_result_t; -// 查询句柄(不透明,可用于取消) -typedef struct hdns_query_s hdns_query_t; +// 查询句柄:hdns_t 是一个事件对象(继承自 hevent_t,与 htimer_t / hio_t 同源), +// 因此可携带 event_id、复用 hevent_set_id/hevent_id。 +typedef struct hdns_s hdns_t; -// 解析完成回调,在 loop 线程被调用;result 仅在回调期间有效 -typedef void (*hdns_cb)(const hdns_result_t* result, void* userdata); +// 解析完成回调,在 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_query_t* hdns_resolve(hloop_t* loop, const char* host, - hdns_cb cb, void* userdata); +hdns_t* hdns_resolve(hloop_t* loop, const char* host, + hdns_cb cb, void* userdata); // 带选项版本 -hdns_query_t* hdns_resolve_ex(hloop_t* loop, const char* host, - const hdns_options_t* opt, - hdns_cb cb, void* userdata); +hdns_t* hdns_resolve_ex(hloop_t* loop, const char* host, + const hdns_options_t* opt, + hdns_cb cb, void* userdata); -// 取消进行中的查询;返回后回调不会再被调用。只能在 loop 线程调用。 -void hdns_cancel(hdns_query_t* query); +// 取消进行中的查询并立即释放;返回后回调不会再被调用。 +// 只能在 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 @@ -107,8 +113,9 @@ void hdns_clear_cache(hloop_t* loop); static int pending = 0; -static void on_resolved(const hdns_result_t* result, void* userdata) { +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) { @@ -137,27 +144,40 @@ int main() { 完整示例见 `examples/hdns_example.c`,性能对比见 `examples/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_options_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`)里**,因此所有继承自 `TcpClientTmpl` 的客户端(`TcpClient`、`WebSocketClient`,以及未来任何 `XXXClient`)都**自动获得**异步 DNS,无需各自编写胶水代码。 +`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()`(首次连接与每次重连都会调用)先用 `hdns_resolve` 异步解析,拿到地址后再在 `startConnectWithAddr()` 建 socket、connect。整个过程**不阻塞事件循环**。旧实现在 loop 线程里调用阻塞的 `getaddrinfo`,会卡住整个 loop。 +- 目标是**域名**时,`createsocket` 只记录 host/port 并**延迟解析**(不阻塞);`startConnect()`(首次连接与每次重连都会调用)先用 `EventLoop::resolveDns` 异步解析,拿到地址后再在 `startConnectWithAddr()` 建 socket、connect。整个过程**不阻塞事件循环**。旧实现在 loop 线程里调用阻塞的 `getaddrinfo`,会卡住整个 loop。 - 每次连接/重连都会重新解析,自动应对 DNS 变化; -- 对象析构或主动 `closesocket()` 时,会自动取消在途的解析查询,避免悬垂回调。 +- 对象只持有 `DnsID`;析构或主动 `closesocket()` 时 `cancelDns` 即可,**无悬垂指针风险**(销毁客户端时查询仍在途也安全,由 `unittest/dns_lifetime_test` 覆盖)。 - 若解析失败且无可用历史地址,会走正常的失败/重连回调。 > 新增客户端零成本:只要继承 `TcpClientTmpl` 并用 `createsocket(port, host)` + `start()`,就自动拥有异步 DNS,不需要实现任何 `onDnsResolved` 之类的回调。 ### AsyncHttpClient -`AsyncHttpClient` 不继承 `TcpClientTmpl`(自带连接池等逻辑),单独接入: +`AsyncHttpClient` 不继承 `TcpClientTmpl`(自带连接池等逻辑),单独接入,但同样使用 `EventLoop::resolveDns`: - 请求 URL 里是**数字 IP**(或 Unix Domain Socket)时,走原有同步快速路径; -- 请求 URL 里是**域名**时,`doTask` 会先用 `hdns_resolve` 异步解析,回调里再建立连接、发送请求——同样**不阻塞 loop**。旧实现直接在 loop 线程里对域名做阻塞 `getaddrinfo`。 -- 客户端析构时会取消所有在途解析并释放其上下文,避免泄漏与悬垂回调。 +- 请求 URL 里是**域名**时,`doTask` 会先用 `EventLoop::resolveDns` 异步解析,回调里再建立连接、发送请求——同样**不阻塞 loop**。旧实现直接在 loop 线程里对域名做阻塞 `getaddrinfo`。 +- 查询生命周期由 `EventLoop` 的 `DnsID` 映射管理,随 loop 拆除统一回收,无需客户端手工跟踪。 > 说明:同步的 `HttpClient` / `requests` 请求路径运行在调用方线程(并非在 loop 内),阻塞解析可接受,故该路径维持不变。 diff --git a/event/hdns.c b/event/hdns.c index dc80a54cd..3e66c4edd 100644 --- a/event/hdns.c +++ b/event/hdns.c @@ -452,13 +452,19 @@ typedef struct hdns_cache_entry_s { } hdns_cache_entry_t; // One logical resolve. -struct hdns_query_s { - hloop_t* loop; +// 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... struct hdns_resolver_s* resolver; + int detached; // 1 = cancelled: run to completion, drop result char host[HDNS_NAME_MAXLEN]; hdns_options_t opt; - hdns_cb cb; - void* userdata; + 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 { @@ -486,10 +492,10 @@ 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_query_t + 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 + uint16_t next_txid; // monotonic base for sub-query txids (wire) uint8_t sndbuf[HDNS_UDP_BUFSIZE]; } hdns_resolver_t; @@ -497,8 +503,8 @@ typedef struct hdns_resolver_s { 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_query_t* q); -static void hdns__finish(hdns_query_t* q, int status); +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 @@ -547,7 +553,7 @@ void hdns_resolver_free(hloop_t* loop) { // cancel outstanding queries (no callbacks on teardown) struct list_node *node, *tmp; list_for_each_safe(node, tmp, &r->queries) { - hdns_query_t* q = list_entry(node, hdns_query_t, node); + 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); @@ -642,29 +648,37 @@ void hdns_clear_cache(hloop_t* loop) { //============================================================================== // Fill an hdns_result_t and invoke the user callback, then free the query. -static void hdns__deliver(hdns_query_t* q, int status) { - hdns_result_t result; - 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)); - } - hdns_cb cb = q->cb; +// 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; - // detach + free before callback so cb may safely destroy related objects - if (q->node.next) list_del(&q->node); + 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. The query is already removed from the in-flight list + // so a re-entrant hdns_cancel(q) from the callback cannot double-free. + 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; } - HV_FREE(q); - if (cb) cb(&result, ud); + 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_query_t* q) { +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) { @@ -678,7 +692,7 @@ static void hdns__sort_v4_first(hdns_query_t* q) { } // Called when all requested sub-queries have settled. -static void hdns__complete(hdns_query_t* q) { +static void hdns__complete(hdns_t* q) { hdns_resolver_t* r = q->resolver; int status; if (q->naddrs > 0) { @@ -698,7 +712,7 @@ static void hdns__complete(hdns_query_t* q) { hdns__deliver(q, status); } -static int hdns__all_done(hdns_query_t* q) { +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; } @@ -706,7 +720,7 @@ static int hdns__all_done(hdns_query_t* q) { } // Send (or resend) all not-yet-done sub-queries to the current nameserver. -static void hdns__send_queries(hdns_query_t* q) { +static void hdns__send_queries(hdns_t* q) { hdns_resolver_t* r = q->resolver; hdns__config_lock_init(); hmutex_lock(&s_config_mutex); @@ -763,7 +777,7 @@ static void hdns__send_queries(hdns_query_t* q) { // Per-attempt timeout: retry (rotate nameserver) or give up. static void hdns__on_timeout(htimer_t* timer) { - hdns_query_t* q = (hdns_query_t*)hevent_userdata(timer); + hdns_t* q = (hdns_t*)hevent_userdata(timer); if (!q) return; q->timer = NULL; // this single-shot timer auto-deletes after firing @@ -785,7 +799,7 @@ static void hdns__on_timeout(htimer_t* timer) { } // Terminal helper for setup errors (async-safe: schedules delivery). -static void hdns__finish(hdns_query_t* q, int status) { +static void hdns__finish(hdns_t* q, int status) { q->naddrs = 0; q->any_error = status; hdns__deliver(q, status); @@ -801,7 +815,7 @@ static void hdns__on_udp_read(hio_t* io, void* buf, int readbytes) { // find the query + sub-query for this txid struct list_node* node; list_for_each(node, &r->queries) { - hdns_query_t* q = list_entry(node, hdns_query_t, node); + 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; @@ -832,7 +846,7 @@ static void hdns__on_udp_read(hio_t* io, void* buf, int readbytes) { // Deferred delivery for immediate results (numeric IP / hosts / cache hit). static void hdns__on_defer(htimer_t* timer) { - hdns_query_t* q = (hdns_query_t*)hevent_userdata(timer); + hdns_t* q = (hdns_t*)hevent_userdata(timer); if (!q) return; q->defer_timer = NULL; // single-shot auto-deletes hdns__complete(q); @@ -841,13 +855,13 @@ static void hdns__on_defer(htimer_t* timer) { // 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_query_t* q = (hdns_query_t*)hevent_userdata(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_query_t* q) { +static void hdns__defer_complete(hdns_t* q) { // schedule completion on the next loop tick (1ms) so cb never runs // re-entrantly inside hdns_resolve(). q->defer_timer = htimer_add(q->loop, hdns__on_defer, 1, 1); @@ -859,9 +873,9 @@ static void hdns__defer_complete(hdns_query_t* q) { } } -hdns_query_t* hdns_resolve_ex(hloop_t* loop, const char* host, - const hdns_options_t* opt, - hdns_cb cb, void* userdata) { +hdns_t* hdns_resolve_ex(hloop_t* loop, const char* host, + const hdns_options_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; @@ -869,13 +883,14 @@ hdns_query_t* hdns_resolve_ex(hloop_t* loop, const char* host, hdns_resolver_t* r = hdns__resolver_get(loop); if (!r) return NULL; - hdns_query_t* q; + 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->cb = cb; + q->dns_cb = cb; q->userdata = userdata; q->min_ttl = HDNS_CACHE_MAX_TTL; @@ -896,6 +911,10 @@ hdns_query_t* hdns_resolve_ex(hloop_t* loop, const char* host, // 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)); @@ -962,12 +981,12 @@ hdns_query_t* hdns_resolve_ex(hloop_t* loop, const char* host, return q; } -hdns_query_t* hdns_resolve(hloop_t* loop, const char* host, - hdns_cb cb, void* userdata) { +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_query_t* q) { +void hdns_cancel(hdns_t* q) { if (!q) return; if (q->node.next) list_del(&q->node); if (q->timer) { htimer_del(q->timer); q->timer = NULL; } diff --git a/event/hdns.h b/event/hdns.h index f2db487f6..3720cbb88 100644 --- a/event/hdns.h +++ b/event/hdns.h @@ -80,14 +80,26 @@ typedef struct hdns_result_s { sockaddr_u addrs[HDNS_MAX_ADDRS]; // A/AAAA merged (IPv4 first), port = 0 } hdns_result_t; -// opaque, cancelable handle -typedef struct hdns_query_s hdns_query_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. - * NOTE: the result pointer is only valid during the callback. + * @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)(const hdns_result_t* result, void* userdata); +typedef void (*hdns_cb)(hdns_t* query, const hdns_result_t* result, void* userdata); BEGIN_EXTERN_C @@ -101,18 +113,19 @@ BEGIN_EXTERN_C * * @return a query handle, or NULL on immediate failure (invalid params / OOM). */ -HV_EXPORT hdns_query_t* hdns_resolve(hloop_t* loop, const char* host, - hdns_cb cb, void* userdata); +HV_EXPORT hdns_t* hdns_resolve(hloop_t* loop, const char* host, + hdns_cb cb, void* userdata); -HV_EXPORT hdns_query_t* hdns_resolve_ex(hloop_t* loop, const char* host, - const hdns_options_t* opt, - hdns_cb cb, void* userdata); +HV_EXPORT hdns_t* hdns_resolve_ex(hloop_t* loop, const char* host, + const hdns_options_t* opt, + hdns_cb cb, void* userdata); /* - * Cancel an in-flight query. After this returns, @cb will NOT be called and - * @query is invalid. MUST be called from the loop thread. + * 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_query_t* query); +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); 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..5187cccff 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_options_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 649d6c6cf..c285d4775 100644 --- a/evpp/TcpClient.h +++ b/evpp/TcpClient.h @@ -28,7 +28,7 @@ class TcpClientEventLoopTmpl { reconn_setting = NULL; unpack_setting = NULL; reconn_timer_id = INVALID_TIMER_ID; - dns_query = NULL; + dns_id = INVALID_DNS_ID; } virtual ~TcpClientEventLoopTmpl() { @@ -151,40 +151,38 @@ class TcpClientEventLoopTmpl { } // @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_options_t opt; opt.family = HDNS_QUERY_BOTH; if (connect_timeout > 0) opt.timeout_ms = connect_timeout; - dns_query = hdns_resolve_ex(loop_->loop(), remote_host.c_str(), &opt, - &TcpClientEventLoopTmpl::onDnsResolved, this); - if (dns_query == NULL) { + 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); + onResolveFailed(); + 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; fall back to synchronous path return startConnectWithAddr(); } return 0; } - // @internal: hdns callback (runs in loop thread). - static void onDnsResolved(const hdns_result_t* result, void* userdata) { - TcpClientEventLoopTmpl* self = (TcpClientEventLoopTmpl*)userdata; - self->dns_query = NULL; // handle is invalid after the callback - if (result->status == HDNS_STATUS_OK && result->naddrs > 0) { - // adopt the first resolved address, keep the target port - self->remote_addr = result->addrs[0]; - sockaddr_set_port(&self->remote_addr, self->remote_port); - } else if (self->remote_addr.sa.sa_family == 0) { - // resolve failed and we have no previously-resolved address to fall - // back to. Report failure and drive reconnect (if enabled). - hloge("resolve %s failed, status=%d", self->remote_host.c_str(), result->status); - self->onResolveFailed(); - return; - } - // else: resolve failed but keep the previous remote_addr; the connect - // attempt will fail and drive the normal reconnect path. - self->startConnectWithAddr(); - } - // @internal: DNS resolution failed with no usable address. void onResolveFailed() { if (onConnection && channel) { @@ -366,16 +364,16 @@ class TcpClientEventLoopTmpl { // Cancel any in-flight async DNS query (loop thread only). void cancelDnsQuery() { - if (dns_query != NULL) { - hdns_cancel(dns_query); - dns_query = NULL; + if (dns_id != INVALID_DNS_ID) { + loop_->cancelDns(dns_id); + dns_id = INVALID_DNS_ID; } } private: EventLoopPtr loop_; TimerID reconn_timer_id; - hdns_query_t* dns_query; + DnsID dns_id; }; template diff --git a/examples/hdns_benchmark.c b/examples/hdns_benchmark.c index 8e04958da..6e2e61d9c 100644 --- a/examples/hdns_benchmark.c +++ b/examples/hdns_benchmark.c @@ -38,7 +38,8 @@ static const char* default_hosts[] = { static int g_pending = 0; static int g_ok = 0; -static void on_resolved(const hdns_result_t* result, void* userdata) { +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); diff --git a/examples/hdns_example.c b/examples/hdns_example.c index 8d6a39462..bffef3f0a 100644 --- a/examples/hdns_example.c +++ b/examples/hdns_example.c @@ -16,7 +16,8 @@ static int pending = 0; -static void on_resolved(const hdns_result_t* result, void* userdata) { +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); diff --git a/http/client/AsyncHttpClient.cpp b/http/client/AsyncHttpClient.cpp index 3b148b421..f77f238a6 100644 --- a/http/client/AsyncHttpClient.cpp +++ b/http/client/AsyncHttpClient.cpp @@ -37,8 +37,9 @@ int AsyncHttpClient::doTask(const HttpClientTaskPtr& task) { const char* host = req->host.c_str(); // If host is a numeric IP (or UDS), resolve synchronously (fast path). - // Otherwise resolve the hostname asynchronously via hdns so the event loop - // is never blocked by getaddrinfo. + // 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)); @@ -50,51 +51,30 @@ int AsyncHttpClient::doTask(const HttpClientTaskPtr& task) { return doTaskWithAddr(task, &peeraddr); } - // async resolve hostname, then continue in onDnsResolved - DnsContext* dctx = new DnsContext(); - dctx->client = this; - dctx->task = task; - dctx->query = NULL; - dns_queries.insert(dctx); - hdns_options_t opt; if (req->connect_timeout > 0) opt.timeout_ms = req->connect_timeout * 1000; - dctx->query = hdns_resolve_ex(EventLoopThread::hloop(), host, &opt, - &AsyncHttpClient::onDnsResolved, dctx); - if (dctx->query == NULL) { - dns_queries.erase(dctx); - delete dctx; + 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; } -// hdns callback (runs in loop thread). -void AsyncHttpClient::onDnsResolved(const hdns_result_t* result, void* userdata) { - DnsContext* dctx = (DnsContext*)userdata; - AsyncHttpClient* self = dctx->client; - HttpClientTaskPtr task = dctx->task; - // handle is invalid after this callback; drop the tracking context. - self->dns_queries.erase(dctx); - delete dctx; - - if (result->status != HDNS_STATUS_OK || result->naddrs <= 0) { - hloge("resolve host %s failed, status=%d", result->host, result->status); - if (task->cb) task->cb(NULL); - return; - } - - // adopt the first resolved address and set the target port. - sockaddr_u peeraddr = result->addrs[0]; - sockaddr_set_port(&peeraddr, task->req->port); - - int err = self->doTaskWithAddr(task, &peeraddr); - if (err != 0 && task->cb) { - task->cb(NULL); - } -} - // Continue the request once the peer address is known. int AsyncHttpClient::doTaskWithAddr(const HttpClientTaskPtr& task, const sockaddr_u* paddr) { const HttpRequestPtr& req = task->req; diff --git a/http/client/AsyncHttpClient.h b/http/client/AsyncHttpClient.h index 048409dcc..31fd262fb 100644 --- a/http/client/AsyncHttpClient.h +++ b/http/client/AsyncHttpClient.h @@ -3,11 +3,9 @@ #include #include -#include #include "EventLoopThread.h" #include "Channel.h" -#include "hdns.h" #include "HttpMessage.h" #include "HttpParser.h" @@ -112,19 +110,10 @@ class HV_EXPORT AsyncHttpClient : private EventLoopThread { } } ~AsyncHttpClient() { - // Stop (and, for an owned loop, free) the event loop first. An owned - // loop uses HLOOP_FLAG_AUTO_FREE, so joining the loop thread runs - // hloop_free() -> hdns_resolver_free(), which cancels and frees every - // in-flight hdns_query_t. After stop() the loop thread is joined, so no - // resolve callback can race with teardown. + // 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); - // The resolver already freed the hdns_query_t objects during loop - // teardown, so DO NOT call hdns_cancel() here (that would be a - // use-after-free). Just free our own per-request context shells. - for (auto* dctx : dns_queries) { - delete dctx; - } - dns_queries.clear(); } // thread-safe @@ -143,16 +132,8 @@ class HV_EXPORT AsyncHttpClient : private EventLoopThread { } int doTask(const HttpClientTaskPtr& task); - // async DNS resolve context for one task (heap-allocated, tracked so it can - // be cancelled/freed on teardown). - struct DnsContext { - AsyncHttpClient* client; - HttpClientTaskPtr task; - hdns_query_t* query; - }; // @internal: continue doTask after the peer address is known. int doTaskWithAddr(const HttpClientTaskPtr& task, const sockaddr_u* peeraddr); - static void onDnsResolved(const hdns_result_t* result, void* userdata); static int sendRequest(const SocketChannelPtr& channel); @@ -182,8 +163,6 @@ class HV_EXPORT AsyncHttpClient : private EventLoopThread { std::map channels; // peeraddr => ConnPool std::map> conn_pools; - // in-flight async DNS resolve contexts (for cancel/free on teardown) - std::set dns_queries; }; } diff --git a/scripts/unittest.sh b/scripts/unittest.sh index 9b76bd9ad..13888fa87 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -38,6 +38,9 @@ fi if [ -x bin/tcpclient_dns_test ]; then bin/tcpclient_dns_test fi +if [ -x bin/dns_lifetime_test ]; then + bin/dns_lifetime_test +fi if [ -x bin/asynchttp_dns_test ]; then bin/asynchttp_dns_test fi diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index cc5e1cf2f..dda4b7317 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -119,6 +119,12 @@ 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) + +# regression: destroying a client mid-resolve must not UAF/double-free +add_executable(dns_lifetime_test dns_lifetime_test.cpp) +target_include_directories(dns_lifetime_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp) +target_link_libraries(dns_lifetime_test ${HV_LIBRARIES}) +list(APPEND EVPP_DNS_UNITTEST_TARGETS dns_lifetime_test) endif() # ------http: async dns in AsyncHttpClient------ diff --git a/unittest/dns_lifetime_test.cpp b/unittest/dns_lifetime_test.cpp new file mode 100644 index 000000000..0c6628ba0 --- /dev/null +++ b/unittest/dns_lifetime_test.cpp @@ -0,0 +1,49 @@ +/* + * dns_lifetime_test — regression test for the DNS handle lifetime redesign. + * + * Destroys a TcpClient while a hostname DNS resolve is still in flight. With the + * old raw-pointer handle this could double-free / use-after-free (the loop's + * resolver frees the query at teardown while the client still held the pointer, + * or a resolve callback fired into a destroyed client). With the DnsID model + * (EventLoop keeps a DnsID -> query map, stale ids no-op) this must be clean. + * + * The client targets a hostname pointed at a black-hole nameserver so the + * resolve stays in flight; we then destroy the client immediately. + */ + +#include +#include + +#include "TcpClient.h" +#include "htime.h" + +using namespace hv; + +int main() { + // Run several iterations to shake out races/UAF under repeated teardown. + for (int i = 0; i < 20; ++i) { + auto cli = std::make_shared(); + // A hostname (not numeric IP) forces the async DNS path. Use a .invalid + // TLD (RFC 6761: guaranteed not to resolve) so the query stays in flight + // / fails, never connecting. + cli->createsocket(80, "nonexistent-host.invalid"); + cli->start(); + // Give the resolve just enough time to be issued but not complete, + // then destroy the client while it's (very likely) still in flight. + hv_msleep(i % 3); // 0/1/2 ms: vary the teardown window + cli->stop(); + cli.reset(); // destroy client mid-resolve + } + + // Also exercise immediate destroy with zero delay (resolve definitely in flight). + 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("\nPASS: destroying TcpClient mid-resolve is clean (no UAF/double-free)\n"); + return 0; +} diff --git a/unittest/hdns_test.c b/unittest/hdns_test.c index 95fd8dbe3..1a5fefac2 100644 --- a/unittest/hdns_test.c +++ b/unittest/hdns_test.c @@ -127,7 +127,8 @@ typedef struct { hloop_t* loop; } expect_t; -static void on_expect(const hdns_result_t* result, void* userdata) { +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); @@ -146,7 +147,7 @@ static void run_expect(hloop_t* loop, const char* host, const hdns_options_t* op e.want_status = want_status; e.want_min_addrs = want_min_addrs; e.loop = loop; - hdns_query_t* q = hdns_resolve_ex(loop, host, opt, on_expect, &e); + hdns_t* q = hdns_resolve_ex(loop, host, opt, on_expect, &e); assert(q != NULL); hloop_run(loop); assert(e.got == 1); @@ -154,8 +155,8 @@ static void run_expect(hloop_t* loop, const char* host, const hdns_options_t* op // callback that must NEVER be invoked (cancel test) static int g_cancel_cb_called = 0; -static void on_never(const hdns_result_t* result, void* userdata) { - (void)result; (void)userdata; +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) { @@ -225,7 +226,7 @@ int main() { 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_query_t* q = hdns_resolve_ex(loop, "cancel.test", &oc, on_never, NULL); + 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 From 41b53799ec881c1a34ae8d49aa829103fa5632bc Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 23 Jul 2026 15:48:57 +0800 Subject: [PATCH 07/17] fix(evpp): notify onConnection when first-attempt DNS resolve fails With async resolution the hostname is resolved before the socket/channel exists. If that first resolve failed, the notification was guarded by `if (onConnection && channel)` -- but channel was still NULL, so the callback was silently dropped and a client without reconnect never learned the connect failed (it looked stuck "connecting"). Fix: if no channel exists yet, create one wrapping a NULL hio_t. Channel's methods all guard against a null io (isConnected()==false, peeraddr()=="", write/close no-op), so the user receives a valid, non-connected channel that matches the connect-failure contract -- without fabricating a real socket. Also factor the shared "notify disconnect via onConnection, then reconnect if configured" logic into notifyDisconnectThenReconnect(), used by both the onclose path and the DNS-failure path (onDnsResolveFailed). This removes the duplicated block and ensures the DNS-failure path also snapshots the reconnect flag into a local BEFORE the user callback -- onConnection may delete the client, so no member is touched afterwards (a user destroying the object must first call setReconnect(NULL), matching the existing onclose contract). - unittest/dns_resolvefail_test: TcpClient to an unresolvable .invalid host with no reconnect must still get onConnection(disconnected); the test also calls channel->peeraddr() to confirm a NULL-io channel is safe to use. - wired into Makefile / CMake / unittest.sh; doc note in docs/cn/hdns.md. All DNS tests pass (Makefile + CMake), no regression to connect/reconnect. --- Makefile | 3 +- docs/cn/hdns.md | 2 +- evpp/TcpClient.h | 35 ++++++++++++++------- scripts/unittest.sh | 3 ++ unittest/CMakeLists.txt | 6 ++++ unittest/dns_resolvefail_test.cpp | 52 +++++++++++++++++++++++++++++++ 6 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 unittest/dns_resolvefail_test.cpp diff --git a/Makefile b/Makefile index b19cedde4..19d052781 100644 --- a/Makefile +++ b/Makefile @@ -319,6 +319,7 @@ 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 $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -o bin/dns_lifetime_test unittest/dns_lifetime_test.cpp -Llib -lhv -pthread + $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -o bin/dns_resolvefail_test unittest/dns_resolvefail_test.cpp -Llib -lhv -pthread ifeq ($(WITH_HTTP), yes) ifeq ($(WITH_HTTP_CLIENT), yes) ifeq ($(WITH_HTTP_SERVER), yes) @@ -338,7 +339,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 bin/dns_lifetime_test bin/asynchttp_dns_test bin/websocket_dns_test + $(RM) bin/tcpclient_dns_test bin/dns_lifetime_test bin/dns_resolvefail_test bin/asynchttp_dns_test bin/websocket_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/docs/cn/hdns.md b/docs/cn/hdns.md index 41800cd95..1a55099b8 100644 --- a/docs/cn/hdns.md +++ b/docs/cn/hdns.md @@ -167,7 +167,7 @@ void cancelDns(DnsID id); // 线程安全;失效 id 自动 no-op - 目标是**域名**时,`createsocket` 只记录 host/port 并**延迟解析**(不阻塞);`startConnect()`(首次连接与每次重连都会调用)先用 `EventLoop::resolveDns` 异步解析,拿到地址后再在 `startConnectWithAddr()` 建 socket、connect。整个过程**不阻塞事件循环**。旧实现在 loop 线程里调用阻塞的 `getaddrinfo`,会卡住整个 loop。 - 每次连接/重连都会重新解析,自动应对 DNS 变化; - 对象只持有 `DnsID`;析构或主动 `closesocket()` 时 `cancelDns` 即可,**无悬垂指针风险**(销毁客户端时查询仍在途也安全,由 `unittest/dns_lifetime_test` 覆盖)。 -- 若解析失败且无可用历史地址,会走正常的失败/重连回调。 +- 解析失败(含首次连接就失败、此时还没有 channel)时,会用一个 NULL-io 的 channel(`isConnected()` 为 false、各方法对空 io 已做防护)走 `onConnection(断开)` 通知,**保证用户总能收到失败回调**(而不是静默丢失);若配置了重连,则继续按重连策略重试。由 `unittest/dns_resolvefail_test` 覆盖。 > 新增客户端零成本:只要继承 `TcpClientTmpl` 并用 `createsocket(port, host)` + `start()`,就自动拥有异步 DNS,不需要实现任何 `onDnsResolved` 之类的回调。 diff --git a/evpp/TcpClient.h b/evpp/TcpClient.h index c285d4775..70b18d6a6 100644 --- a/evpp/TcpClient.h +++ b/evpp/TcpClient.h @@ -169,7 +169,7 @@ class TcpClientEventLoopTmpl { // 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); - onResolveFailed(); + onDnsResolveFailed(); return; } // else: resolve failed but keep the previous remote_addr; the @@ -184,12 +184,29 @@ class TcpClientEventLoopTmpl { } // @internal: DNS resolution failed with no usable address. - void onResolveFailed() { - if (onConnection && channel) { - // channel is not connected; notify disconnect-style callback. + // 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); + } + notifyDisconnectThenReconnect(); + } + + // @internal: notify a disconnect via onConnection, then reconnect if set. + // NOTE: onConnection is a user callback that may delete this object, so we + // snapshot whether to reconnect into a local BEFORE the callback and touch + // no members afterwards (a user destroying the object must first call + // setReconnect(NULL)). Shared by the onclose path and DNS-failure path. + void notifyDisconnectThenReconnect() { + bool reconnect = reconn_setting != NULL; + if (onConnection) { onConnection(channel); } - if (reconn_setting) { + if (reconnect) { startReconnect(); } } @@ -246,13 +263,7 @@ class TcpClientEventLoopTmpl { } }; channel->onclose = [this]() { - bool reconnect = reconn_setting != NULL; - if (onConnection) { - onConnection(channel); - } - if (reconnect) { - startReconnect(); - } + notifyDisconnectThenReconnect(); }; return channel->startConnect(); } diff --git a/scripts/unittest.sh b/scripts/unittest.sh index 13888fa87..79713f4a7 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -41,6 +41,9 @@ fi if [ -x bin/dns_lifetime_test ]; then bin/dns_lifetime_test fi +if [ -x bin/dns_resolvefail_test ]; then + bin/dns_resolvefail_test +fi if [ -x bin/asynchttp_dns_test ]; then bin/asynchttp_dns_test fi diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index dda4b7317..4608d5bf9 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -125,6 +125,12 @@ add_executable(dns_lifetime_test dns_lifetime_test.cpp) target_include_directories(dns_lifetime_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp) target_link_libraries(dns_lifetime_test ${HV_LIBRARIES}) list(APPEND EVPP_DNS_UNITTEST_TARGETS dns_lifetime_test) + +# regression: first-attempt resolve failure must notify onConnection +add_executable(dns_resolvefail_test dns_resolvefail_test.cpp) +target_include_directories(dns_resolvefail_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp) +target_link_libraries(dns_resolvefail_test ${HV_LIBRARIES}) +list(APPEND EVPP_DNS_UNITTEST_TARGETS dns_resolvefail_test) endif() # ------http: async dns in AsyncHttpClient------ diff --git a/unittest/dns_resolvefail_test.cpp b/unittest/dns_resolvefail_test.cpp new file mode 100644 index 000000000..03a7bd4d9 --- /dev/null +++ b/unittest/dns_resolvefail_test.cpp @@ -0,0 +1,52 @@ +/* + * dns_resolvefail_test — regression: a first-attempt DNS resolve failure must + * still notify the user via onConnection (disconnected), even though no channel + * existed at resolve time. Previously the notification was silently dropped + * (channel == NULL), so a no-reconnect client never learned the connect failed. + */ + +#include +#include + +#include "TcpClient.h" +#include "htime.h" + +using namespace hv; + +int main() { + std::atomic got_disconnected_cb{false}; + std::atomic saw_connected{false}; + + auto cli = std::make_shared(); + // hostname that cannot resolve (RFC 6761 .invalid), NO reconnect configured. + cli->createsocket(80, "definitely-nonexistent.invalid"); + cli->onConnection = [&](const SocketChannelPtr& channel) { + // must receive a valid (non-null) channel; user code commonly calls + // channel->isConnected() / peeraddr() first, so nullptr would crash. + if (channel && channel->isConnected()) { + saw_connected = true; + } else { + // exercise the channel API to ensure a NULL-io channel is safe + (void)channel->peeraddr(); + got_disconnected_cb = true; + } + }; + cli->start(); + + // wait for the resolve to fail and the callback to be delivered + 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) { + printf("\nPASS: first-attempt resolve failure notifies onConnection(disconnected)\n"); + return 0; + } + printf("\nFAIL: got_disconnected_cb=%d saw_connected=%d\n", + (int)got_disconnected_cb, (int)saw_connected); + return 1; +} From 72379c160c220f4db6a6c88177ecca8ebf39f280 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 23 Jul 2026 16:48:25 +0800 Subject: [PATCH 08/17] feat(evpp): surface connect failure reason via Channel::error() Let onConnection handlers tell apart *why* a connection failed (DNS resolve failure vs. connect timeout/refused vs. other) through channel->error(). - herr.h: add ERR_DNS_RESOLVE (-1022), next to the socket syscall errors. - Channel: add an error_ member with setError()/error(). error() returns the explicitly-set upper-layer error if any, else falls back to hio_error(io_), and is null-safe (a NULL-io channel no longer crashes error()). - TcpClient::onDnsResolveFailed sets channel->setError(ERR_DNS_RESOLVE) before notifying, so a DNS failure is distinguishable from an IO-layer failure (which continues to surface the underlying errno/herr via hio_error). - dns_resolvefail_test now asserts channel->error() == ERR_DNS_RESOLVE and prints hv_strerror() for it. - doc note in docs/cn/hdns.md. Usage in onConnection: if (!channel->isConnected()) { int err = channel->error(); // ERR_DNS_RESOLVE, ETIMEDOUT, ... } All DNS tests pass (Makefile + CMake). --- base/herr.h | 1 + docs/cn/hdns.md | 1 + evpp/Channel.h | 10 +++++++++- evpp/TcpClient.h | 4 ++++ unittest/dns_resolvefail_test.cpp | 13 +++++++++---- 5 files changed, 24 insertions(+), 5 deletions(-) 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/docs/cn/hdns.md b/docs/cn/hdns.md index 1a55099b8..f0f81a7cd 100644 --- a/docs/cn/hdns.md +++ b/docs/cn/hdns.md @@ -168,6 +168,7 @@ void cancelDns(DnsID id); // 线程安全;失效 id 自动 no-op - 每次连接/重连都会重新解析,自动应对 DNS 变化; - 对象只持有 `DnsID`;析构或主动 `closesocket()` 时 `cancelDns` 即可,**无悬垂指针风险**(销毁客户端时查询仍在途也安全,由 `unittest/dns_lifetime_test` 覆盖)。 - 解析失败(含首次连接就失败、此时还没有 channel)时,会用一个 NULL-io 的 channel(`isConnected()` 为 false、各方法对空 io 已做防护)走 `onConnection(断开)` 通知,**保证用户总能收到失败回调**(而不是静默丢失);若配置了重连,则继续按重连策略重试。由 `unittest/dns_resolvefail_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` 之类的回调。 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/TcpClient.h b/evpp/TcpClient.h index 70b18d6a6..2c7f7aed8 100644 --- a/evpp/TcpClient.h +++ b/evpp/TcpClient.h @@ -4,6 +4,7 @@ #include "hsocket.h" #include "hssl.h" #include "hlog.h" +#include "herr.h" #include "hdns.h" #include "EventLoopThread.h" @@ -193,6 +194,9 @@ class TcpClientEventLoopTmpl { 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(); } diff --git a/unittest/dns_resolvefail_test.cpp b/unittest/dns_resolvefail_test.cpp index 03a7bd4d9..403266922 100644 --- a/unittest/dns_resolvefail_test.cpp +++ b/unittest/dns_resolvefail_test.cpp @@ -9,6 +9,7 @@ #include #include "TcpClient.h" +#include "herr.h" #include "htime.h" using namespace hv; @@ -16,6 +17,7 @@ using namespace hv; int main() { std::atomic got_disconnected_cb{false}; std::atomic saw_connected{false}; + std::atomic reported_error{0}; auto cli = std::make_shared(); // hostname that cannot resolve (RFC 6761 .invalid), NO reconnect configured. @@ -28,6 +30,7 @@ int main() { } else { // exercise the channel API to ensure a NULL-io channel is safe (void)channel->peeraddr(); + reported_error = channel->error(); // should be ERR_DNS_RESOLVE got_disconnected_cb = true; } }; @@ -42,11 +45,13 @@ int main() { cli->stop(); hv_msleep(100); - if (got_disconnected_cb && !saw_connected) { - printf("\nPASS: first-attempt resolve failure notifies onConnection(disconnected)\n"); + if (got_disconnected_cb && !saw_connected && reported_error == ERR_DNS_RESOLVE) { + printf("\nPASS: first-attempt resolve failure notifies onConnection " + "(error=%d %s)\n", reported_error.load(), hv_strerror(reported_error)); return 0; } - printf("\nFAIL: got_disconnected_cb=%d saw_connected=%d\n", - (int)got_disconnected_cb, (int)saw_connected); + printf("\nFAIL: got_disconnected_cb=%d saw_connected=%d error=%d (want %d)\n", + (int)got_disconnected_cb, (int)saw_connected, + reported_error.load(), ERR_DNS_RESOLVE); return 1; } From 1a8992f943a7a606dee71068cdf22ab80e95b8b5 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 23 Jul 2026 18:42:27 +0800 Subject: [PATCH 09/17] test(dns): consolidate async DNS tests; move benchmark to unittest The DNS test set had grown to six files. Consolidate now that the feature is stable: - Merge dns_lifetime_test + dns_resolvefail_test into tcpclient_dns_test.cpp as three test functions (connect+reconnect, destroy-mid-resolve lifetime, resolve-failure notification). One binary, same coverage. - Remove asynchttp_dns_test and websocket_dns_test: the AsyncHttpClient DNS path is now exercised by examples/http_client_test.cpp (URL switched to http://localhost:... to hit the hostname resolve path), and WebSocketClient inherits the exact same base-class DNS path already covered by tcpclient_dns_test. - Move hdns_benchmark from examples/ to unittest/ (it is a measurement/CI aid, not a usage example). - Update Makefile / CMake / scripts/unittest.sh / examples CMake and docs. All DNS tests pass under Makefile and CMake. --- Makefile | 19 +--- docs/cn/hdns.md | 8 +- examples/CMakeLists.txt | 4 - examples/http_client_test.cpp | 2 +- scripts/unittest.sh | 12 -- unittest/CMakeLists.txt | 33 +----- unittest/asynchttp_dns_test.cpp | 76 ------------- unittest/dns_lifetime_test.cpp | 49 -------- unittest/dns_resolvefail_test.cpp | 57 ---------- {examples => unittest}/hdns_benchmark.c | 0 unittest/tcpclient_dns_test.cpp | 144 ++++++++++++++++++------ unittest/websocket_dns_test.cpp | 84 -------------- 12 files changed, 124 insertions(+), 364 deletions(-) delete mode 100644 unittest/asynchttp_dns_test.cpp delete mode 100644 unittest/dns_lifetime_test.cpp delete mode 100644 unittest/dns_resolvefail_test.cpp rename {examples => unittest}/hdns_benchmark.c (100%) delete mode 100644 unittest/websocket_dns_test.cpp diff --git a/Makefile b/Makefile index 19d052781..f6b86583f 100644 --- a/Makefile +++ b/Makefile @@ -70,7 +70,6 @@ EXAMPLES = hmain_test htimer_test hloop_test pipe_test \ udp_proxy_server \ socks5_proxy_server \ hdns_example \ - hdns_benchmark \ multi-acceptor-processes \ multi-acceptor-threads \ one-acceptor-multi-workers \ @@ -177,9 +176,6 @@ socks5_proxy_server: prepare hdns_example: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/hdns_example.c" -hdns_benchmark: prepare - $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/hdns_benchmark.c" - multi-acceptor-processes: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/multi-thread/multi-acceptor-processes.c" @@ -314,20 +310,11 @@ unittest: prepare $(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_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 - $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -o bin/dns_lifetime_test unittest/dns_lifetime_test.cpp -Llib -lhv -pthread - $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -o bin/dns_resolvefail_test unittest/dns_resolvefail_test.cpp -Llib -lhv -pthread -ifeq ($(WITH_HTTP), yes) -ifeq ($(WITH_HTTP_CLIENT), yes) -ifeq ($(WITH_HTTP_SERVER), yes) - $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -o bin/asynchttp_dns_test unittest/asynchttp_dns_test.cpp -Llib -lhv -pthread - $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Iutil -Icpputil -Ievpp -Ihttp -Ihttp/client -Ihttp/server -o bin/websocket_dns_test unittest/websocket_dns_test.cpp -Llib -lhv -pthread -endif -endif -endif 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 @@ -339,7 +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 bin/dns_lifetime_test bin/dns_resolvefail_test bin/asynchttp_dns_test bin/websocket_dns_test + $(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/docs/cn/hdns.md b/docs/cn/hdns.md index f0f81a7cd..d36a83af9 100644 --- a/docs/cn/hdns.md +++ b/docs/cn/hdns.md @@ -142,7 +142,7 @@ int main() { } ``` -完整示例见 `examples/hdns_example.c`,性能对比见 `examples/hdns_benchmark.c`。 +完整示例见 `examples/hdns_example.c`,性能对比见 `unittest/hdns_benchmark.c`。 ## C++ 层:EventLoop::resolveDns @@ -166,8 +166,8 @@ void cancelDns(DnsID id); // 线程安全;失效 id 自动 no-op - 目标是**数字 IP**(或 Unix Domain Socket)时,`createsocket` 走原有同步快速路径立即建 socket,行为不变; - 目标是**域名**时,`createsocket` 只记录 host/port 并**延迟解析**(不阻塞);`startConnect()`(首次连接与每次重连都会调用)先用 `EventLoop::resolveDns` 异步解析,拿到地址后再在 `startConnectWithAddr()` 建 socket、connect。整个过程**不阻塞事件循环**。旧实现在 loop 线程里调用阻塞的 `getaddrinfo`,会卡住整个 loop。 - 每次连接/重连都会重新解析,自动应对 DNS 变化; -- 对象只持有 `DnsID`;析构或主动 `closesocket()` 时 `cancelDns` 即可,**无悬垂指针风险**(销毁客户端时查询仍在途也安全,由 `unittest/dns_lifetime_test` 覆盖)。 -- 解析失败(含首次连接就失败、此时还没有 channel)时,会用一个 NULL-io 的 channel(`isConnected()` 为 false、各方法对空 io 已做防护)走 `onConnection(断开)` 通知,**保证用户总能收到失败回调**(而不是静默丢失);若配置了重连,则继续按重连策略重试。由 `unittest/dns_resolvefail_test` 覆盖。 +- 对象只持有 `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` 之类的回调。 @@ -184,6 +184,6 @@ void cancelDns(DnsID id); // 线程安全;失效 id 自动 no-op ## 性能说明 -`examples/hdns_benchmark.c` 对比“顺序阻塞 `getaddrinfo`” 与 “单 loop 并发 `hdns`” 解析同一批域名:由于异步解析把所有查询一次性发出、由事件循环并发多路复用,总耗时约等于**一次最慢的解析**,而不是所有解析之和;更关键的是解析期间**事件循环始终不被阻塞**,其它 IO / 定时器可以继续运行。 +`unittest/hdns_benchmark.c` 对比“顺序阻塞 `getaddrinfo`” 与 “单 loop 并发 `hdns`” 解析同一批域名:由于异步解析把所有查询一次性发出、由事件循环并发多路复用,总耗时约等于**一次最慢的解析**,而不是所有解析之和;更关键的是解析期间**事件循环始终不被阻塞**,其它 IO / 定时器可以继续运行。 > 注意:单次“命中缓存”的孤立解析,系统 `getaddrinfo`(下游有 `systemd-resolved` 等本地缓存)可能更快;`hdns` 通过尊重 TTL 的进程内缓存来弥补——命中缓存时是进程内查表(微秒级),且同样不阻塞 loop。 diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index ff292c050..a7a8f750e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -13,7 +13,6 @@ list(APPEND EXAMPLES udp_proxy_server socks5_proxy_server hdns_example - hdns_benchmark multi-acceptor-processes multi-acceptor-threads one-acceptor-multi-workers @@ -65,9 +64,6 @@ target_link_libraries(socks5_proxy_server ${HV_LIBRARIES}) add_executable(hdns_example hdns_example.c) target_link_libraries(hdns_example ${HV_LIBRARIES}) -add_executable(hdns_benchmark hdns_benchmark.c) -target_link_libraries(hdns_benchmark ${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/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/scripts/unittest.sh b/scripts/unittest.sh index 79713f4a7..f32edee13 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -38,18 +38,6 @@ fi if [ -x bin/tcpclient_dns_test ]; then bin/tcpclient_dns_test fi -if [ -x bin/dns_lifetime_test ]; then - bin/dns_lifetime_test -fi -if [ -x bin/dns_resolvefail_test ]; then - bin/dns_resolvefail_test -fi -if [ -x bin/asynchttp_dns_test ]; then - bin/asynchttp_dns_test -fi -if [ -x bin/websocket_dns_test ]; then - bin/websocket_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 4608d5bf9..c92a3e1fb 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -95,6 +95,10 @@ 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) @@ -113,38 +117,12 @@ endforeach() list(APPEND REDIS_UNITTEST_TARGETS ${REDIS_LINKED_UNITTEST_TARGETS}) endif() -# ------evpp: async dns in connect path------ +# ------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) - -# regression: destroying a client mid-resolve must not UAF/double-free -add_executable(dns_lifetime_test dns_lifetime_test.cpp) -target_include_directories(dns_lifetime_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp) -target_link_libraries(dns_lifetime_test ${HV_LIBRARIES}) -list(APPEND EVPP_DNS_UNITTEST_TARGETS dns_lifetime_test) - -# regression: first-attempt resolve failure must notify onConnection -add_executable(dns_resolvefail_test dns_resolvefail_test.cpp) -target_include_directories(dns_resolvefail_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp) -target_link_libraries(dns_resolvefail_test ${HV_LIBRARIES}) -list(APPEND EVPP_DNS_UNITTEST_TARGETS dns_resolvefail_test) -endif() - -# ------http: async dns in AsyncHttpClient------ -if(WITH_EVPP AND WITH_HTTP AND WITH_HTTP_CLIENT AND WITH_HTTP_SERVER) -add_executable(asynchttp_dns_test asynchttp_dns_test.cpp) -target_include_directories(asynchttp_dns_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/client ../http/server) -target_link_libraries(asynchttp_dns_test ${HV_LIBRARIES}) -list(APPEND EVPP_DNS_UNITTEST_TARGETS asynchttp_dns_test) - -# WebSocketClient inherits async DNS from the TcpClient base (no WS-specific glue) -add_executable(websocket_dns_test websocket_dns_test.cpp) -target_include_directories(websocket_dns_test PRIVATE .. ../base ../ssl ../event ../util ../cpputil ../evpp ../http ../http/client ../http/server) -target_link_libraries(websocket_dns_test ${HV_LIBRARIES}) -list(APPEND EVPP_DNS_UNITTEST_TARGETS websocket_dns_test) endif() if(UNIX) @@ -178,6 +156,7 @@ add_custom_target(unittest DEPENDS sendmail http_router_test hdns_test + hdns_benchmark ${REDIS_UNITTEST_TARGETS} ${EVPP_DNS_UNITTEST_TARGETS} ) diff --git a/unittest/asynchttp_dns_test.cpp b/unittest/asynchttp_dns_test.cpp deleted file mode 100644 index 66edc1cbe..000000000 --- a/unittest/asynchttp_dns_test.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/* - * asynchttp_dns_test — integration test for async DNS in AsyncHttpClient. - * - * Verifies that an async HTTP request to a *hostname* URL (not a numeric IP) - * resolves the hostname through hdns (async, non-blocking) and completes. - * - * Uses "http://localhost:PORT/ping" against a local HttpServer, so it needs no - * external network (localhost resolves via /etc/hosts inside hdns). - */ - -#include -#include - -#include "HttpServer.h" -#include "HttpClient.h" // http_client_send_async -#include "htime.h" - -using namespace hv; - -int main() { - // 1. start a local HTTP server on a fixed port - int port = 10880; - HttpService router; - router.GET("/ping", [](HttpRequest* req, HttpResponse* resp) { - return resp->String("pong"); - }); - - HttpServer server; - server.registerHttpService(&router); - server.setPort(port); - server.setThreadNum(1); - if (server.start() != 0) { - printf("server start failed on port %d\n", port); - return 1; - } - printf("http server started on localhost:%d\n", port); - hv_msleep(200); - - // 2. issue an async HTTP request to the *hostname* URL - std::atomic done{false}; - std::atomic status_code{0}; - std::string body; - - auto req = std::make_shared(); - char url[128]; - snprintf(url, sizeof(url), "http://localhost:%d/ping", port); - req->url = url; // hostname "localhost" -> exercises async DNS - req->method = HTTP_GET; - req->timeout = 5; - - http_client_send_async(req, [&](const HttpResponsePtr& resp) { - if (resp) { - status_code = resp->status_code; - body = resp->body; - } - done = true; - }); - - // 3. wait for completion - uint64_t start = gettick_ms(); - while (!done && gettick_ms() - start < 6000) { - hv_msleep(20); - } - - server.stop(); - hv_msleep(100); - - if (done && status_code == 200 && body == "pong") { - printf("\nPASS: async HTTP request to hostname resolved via hdns " - "(status=%d body=%s)\n", status_code.load(), body.c_str()); - return 0; - } - printf("\nFAIL: done=%d status=%d body=%s\n", - (int)done, status_code.load(), body.c_str()); - return 2; -} diff --git a/unittest/dns_lifetime_test.cpp b/unittest/dns_lifetime_test.cpp deleted file mode 100644 index 0c6628ba0..000000000 --- a/unittest/dns_lifetime_test.cpp +++ /dev/null @@ -1,49 +0,0 @@ -/* - * dns_lifetime_test — regression test for the DNS handle lifetime redesign. - * - * Destroys a TcpClient while a hostname DNS resolve is still in flight. With the - * old raw-pointer handle this could double-free / use-after-free (the loop's - * resolver frees the query at teardown while the client still held the pointer, - * or a resolve callback fired into a destroyed client). With the DnsID model - * (EventLoop keeps a DnsID -> query map, stale ids no-op) this must be clean. - * - * The client targets a hostname pointed at a black-hole nameserver so the - * resolve stays in flight; we then destroy the client immediately. - */ - -#include -#include - -#include "TcpClient.h" -#include "htime.h" - -using namespace hv; - -int main() { - // Run several iterations to shake out races/UAF under repeated teardown. - for (int i = 0; i < 20; ++i) { - auto cli = std::make_shared(); - // A hostname (not numeric IP) forces the async DNS path. Use a .invalid - // TLD (RFC 6761: guaranteed not to resolve) so the query stays in flight - // / fails, never connecting. - cli->createsocket(80, "nonexistent-host.invalid"); - cli->start(); - // Give the resolve just enough time to be issued but not complete, - // then destroy the client while it's (very likely) still in flight. - hv_msleep(i % 3); // 0/1/2 ms: vary the teardown window - cli->stop(); - cli.reset(); // destroy client mid-resolve - } - - // Also exercise immediate destroy with zero delay (resolve definitely in flight). - 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("\nPASS: destroying TcpClient mid-resolve is clean (no UAF/double-free)\n"); - return 0; -} diff --git a/unittest/dns_resolvefail_test.cpp b/unittest/dns_resolvefail_test.cpp deleted file mode 100644 index 403266922..000000000 --- a/unittest/dns_resolvefail_test.cpp +++ /dev/null @@ -1,57 +0,0 @@ -/* - * dns_resolvefail_test — regression: a first-attempt DNS resolve failure must - * still notify the user via onConnection (disconnected), even though no channel - * existed at resolve time. Previously the notification was silently dropped - * (channel == NULL), so a no-reconnect client never learned the connect failed. - */ - -#include -#include - -#include "TcpClient.h" -#include "herr.h" -#include "htime.h" - -using namespace hv; - -int main() { - std::atomic got_disconnected_cb{false}; - std::atomic saw_connected{false}; - std::atomic reported_error{0}; - - auto cli = std::make_shared(); - // hostname that cannot resolve (RFC 6761 .invalid), NO reconnect configured. - cli->createsocket(80, "definitely-nonexistent.invalid"); - cli->onConnection = [&](const SocketChannelPtr& channel) { - // must receive a valid (non-null) channel; user code commonly calls - // channel->isConnected() / peeraddr() first, so nullptr would crash. - if (channel && channel->isConnected()) { - saw_connected = true; - } else { - // exercise the channel API to ensure a NULL-io channel is safe - (void)channel->peeraddr(); - reported_error = channel->error(); // should be ERR_DNS_RESOLVE - got_disconnected_cb = true; - } - }; - cli->start(); - - // wait for the resolve to fail and the callback to be delivered - 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("\nPASS: first-attempt resolve failure notifies onConnection " - "(error=%d %s)\n", reported_error.load(), hv_strerror(reported_error)); - return 0; - } - printf("\nFAIL: got_disconnected_cb=%d saw_connected=%d error=%d (want %d)\n", - (int)got_disconnected_cb, (int)saw_connected, - reported_error.load(), ERR_DNS_RESOLVE); - return 1; -} diff --git a/examples/hdns_benchmark.c b/unittest/hdns_benchmark.c similarity index 100% rename from examples/hdns_benchmark.c rename to unittest/hdns_benchmark.c diff --git a/unittest/tcpclient_dns_test.cpp b/unittest/tcpclient_dns_test.cpp index 3a2f7b879..9e807fc55 100644 --- a/unittest/tcpclient_dns_test.cpp +++ b/unittest/tcpclient_dns_test.cpp @@ -1,49 +1,54 @@ /* - * tcpclient_dns_test — integration test for async DNS in the evpp connect path. + * tcpclient_dns_test — integration tests for async DNS in the evpp connect path. * - * Verifies that TcpClient with a *hostname* target (not a numeric IP) connects - * successfully and that reconnect re-resolves the hostname through the async - * resolver (TcpClientEventLoopTmpl::startResolveThenConnect) without blocking - * the event loop. - * - * Uses "localhost" (resolved via /etc/hosts by hdns) against a local TCP - * server, so it needs no external network. + * 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; -static std::atomic g_connect_count{0}; -static std::atomic g_reconnected{false}; +// 1. hostname connect + async re-resolve on reconnect +static int test_connect_and_reconnect() { + std::atomic connect_count{0}; + std::atomic reconnected{false}; -int main() { - // 1. start a TCP echo server on a fixed local port int port = 40833; TcpServer srv; int listenfd = srv.createsocket(port, "127.0.0.1"); if (listenfd < 0) { - printf("createsocket failed on port %d\n", port); - return 1; + printf("[connect] createsocket failed on port %d\n", port); + return -1; } - printf("server listening on 127.0.0.1:%d\n", port); srv.onMessage = [](const SocketChannelPtr& ch, Buffer* buf) { ch->write(buf); // echo }; srv.start(); - // 2. TcpClient targeting "localhost" (a hostname -> exercises DNS path) + // TcpClient targeting "localhost" (a hostname -> exercises DNS path) auto cli = std::make_shared(); int connfd = cli->createsocket(port, "localhost"); if (connfd < 0) { - printf("client createsocket failed\n"); - return 2; + printf("[connect] client createsocket failed\n"); + return -1; } reconn_setting_t reconn; @@ -55,42 +60,113 @@ int main() { cli->onConnection = [&](const SocketChannelPtr& channel) { if (channel->isConnected()) { - int n = ++g_connect_count; - printf("connected to %s (count=%d)\n", channel->peeraddr().c_str(), n); + 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. - // channel->close() is thread-safe and runs in the loop thread. setTimeout(200, [channel](TimerID) { - printf("closing to trigger reconnect...\n"); channel->close(); }); } else if (n >= 2) { - g_reconnected = true; + reconnected = true; } - } else { - printf("disconnected\n"); } }; cli->start(); - // 3. run until reconnect confirmed or timeout uint64_t start = gettick_ms(); - while (!g_reconnected && gettick_ms() - start < 8000) { + while (!reconnected && gettick_ms() - start < 8000) { hv_msleep(50); } - // stop reconnect first (thread-safe), then let the client shut down. cli->setReconnect(NULL); cli->stop(true); srv.stop(); hv_msleep(200); - if (g_connect_count >= 2 && g_reconnected) { - printf("\nPASS: hostname connect + async re-resolve reconnect worked " - "(connects=%d)\n", g_connect_count.load()); + 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("\nFAIL: connects=%d reconnected=%d\n", - g_connect_count.load(), (int)g_reconnected); - return 3; + printf("\ntcpclient_dns_test FAILED\n"); + return 1; } diff --git a/unittest/websocket_dns_test.cpp b/unittest/websocket_dns_test.cpp deleted file mode 100644 index 0441b040a..000000000 --- a/unittest/websocket_dns_test.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/* - * websocket_dns_test — integration test proving async DNS works generically - * for WebSocketClient with NO WebSocket-specific DNS glue. - * - * WebSocketClient extends TcpClientTmpl, so it inherits the async hostname - * resolution baked into TcpClientEventLoopTmpl::startConnect(). Connecting to a - * hostname URL (ws://localhost:PORT/) must resolve via hdns without blocking - * the loop, then complete the WS handshake. - * - * Uses "localhost" against a local WebSocketServer, so it needs no external - * network (localhost resolves via /etc/hosts inside hdns). - */ - -#include -#include - -#include "WebSocketServer.h" -#include "WebSocketClient.h" -#include "htime.h" - -using namespace hv; - -int main() { - int port = 19999; - - // 1. start a local WebSocket echo server - WebSocketService ws; - ws.onmessage = [](const WebSocketChannelPtr& channel, const std::string& msg) { - channel->send(msg); // echo - }; - - WebSocketServer server; - server.port = port; - server.registerWebSocketService(&ws); - if (server.start() != 0) { - printf("websocket server start failed on port %d\n", port); - return 1; - } - printf("websocket server started on localhost:%d\n", port); - hv_msleep(200); - - // 2. connect via a *hostname* URL (exercises generic async DNS in the base) - std::atomic opened{false}; - std::atomic got_echo{false}; - std::string received; - - WebSocketClient cli; - cli.onopen = [&]() { - printf("ws onopen\n"); - opened = true; - cli.send("hello"); - }; - cli.onmessage = [&](const std::string& msg) { - printf("ws onmessage: %s\n", msg.c_str()); - received = msg; - got_echo = true; - }; - cli.onclose = [&]() { - printf("ws onclose\n"); - }; - - char url[128]; - snprintf(url, sizeof(url), "ws://localhost:%d/", port); - cli.open(url); - - // 3. wait for echo or timeout - uint64_t start = gettick_ms(); - while (!got_echo && gettick_ms() - start < 6000) { - hv_msleep(20); - } - - cli.close(); - server.stop(); - hv_msleep(100); - - if (opened && got_echo && received == "hello") { - printf("\nPASS: websocket connect to hostname resolved via hdns " - "(echo=%s)\n", received.c_str()); - return 0; - } - printf("\nFAIL: opened=%d got_echo=%d received=%s\n", - (int)opened, (int)got_echo, received.c_str()); - return 2; -} From 525998d2bf484ec986b9d98909c847261203d749 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 23 Jul 2026 19:21:00 +0800 Subject: [PATCH 10/17] refactor(hdns): rename hdns_options_t -> hdns_setting_t Align the DNS options struct name with libhv's existing convention, which uses singular *_setting_t (unpack_setting_t, kcp_setting_t, reconn_setting_t) rather than *_options_t / *_settings_t. Pure rename of the type/struct (hdns_options_s/_t -> hdns_setting_s/_t); no field or behavior change. Updates hdns.h/.c, EventLoop::resolveDns, TcpClient, AsyncHttpClient, tests and docs. All DNS tests pass under Makefile and CMake. --- docs/cn/hdns.md | 8 ++++---- event/hdns.c | 4 ++-- event/hdns.h | 8 ++++---- evpp/EventLoop.h | 2 +- evpp/TcpClient.h | 2 +- http/client/AsyncHttpClient.cpp | 2 +- unittest/hdns_benchmark.c | 2 +- unittest/hdns_test.c | 14 +++++++------- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/cn/hdns.md b/docs/cn/hdns.md index d36a83af9..66bbe72a6 100644 --- a/docs/cn/hdns.md +++ b/docs/cn/hdns.md @@ -42,13 +42,13 @@ typedef enum { } hdns_family_e; // 查询选项(可选;传 NULL 使用默认值) -typedef struct hdns_options_s { +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_options_t; +} hdns_setting_t; // 解析结果 typedef struct hdns_result_s { @@ -73,7 +73,7 @@ hdns_t* hdns_resolve(hloop_t* loop, const char* host, // 带选项版本 hdns_t* hdns_resolve_ex(hloop_t* loop, const char* host, - const hdns_options_t* opt, + const hdns_setting_t* opt, hdns_cb cb, void* userdata); // 取消进行中的查询并立即释放;返回后回调不会再被调用。 @@ -150,7 +150,7 @@ C++ 客户端不直接用 C 层裸指针句柄,而是通过 `EventLoop` 提供 ```cpp // 返回免疫 use-after-free 的 DnsID(0 = 失败);cb 在 loop 线程回调 -DnsID resolveDns(const char* host, DnsCallback cb, const hdns_options_t* opt = NULL); +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) ``` diff --git a/event/hdns.c b/event/hdns.c index 3e66c4edd..3e09515f8 100644 --- a/event/hdns.c +++ b/event/hdns.c @@ -463,7 +463,7 @@ struct hdns_s { struct hdns_resolver_s* resolver; int detached; // 1 = cancelled: run to completion, drop result char host[HDNS_NAME_MAXLEN]; - hdns_options_t opt; + 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) @@ -874,7 +874,7 @@ static void hdns__defer_complete(hdns_t* q) { } hdns_t* hdns_resolve_ex(hloop_t* loop, const char* host, - const hdns_options_t* opt, + const hdns_setting_t* opt, hdns_cb cb, void* userdata) { if (!loop || !host || !cb) return NULL; size_t hlen = strlen(host); diff --git a/event/hdns.h b/event/hdns.h index 3720cbb88..4de0e5606 100644 --- a/event/hdns.h +++ b/event/hdns.h @@ -55,7 +55,7 @@ typedef enum { HDNS_QUERY_BOTH = 0x03, // A + AAAA (default) } hdns_family_e; -typedef struct hdns_options_s { +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 @@ -63,7 +63,7 @@ typedef struct hdns_options_s { const char* nameserver; // optional override "ip" or "ip:port", NULL = auto #ifdef __cplusplus - hdns_options_s() { + hdns_setting_s() { family = HDNS_QUERY_BOTH; timeout_ms = HDNS_DEFAULT_TIMEOUT_MS; retries = HDNS_DEFAULT_RETRIES; @@ -71,7 +71,7 @@ typedef struct hdns_options_s { nameserver = NULL; } #endif -} hdns_options_t; +} hdns_setting_t; typedef struct hdns_result_s { int status; // 0:ok <0:herr code @@ -117,7 +117,7 @@ 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_options_t* opt, + const hdns_setting_t* opt, hdns_cb cb, void* userdata); /* diff --git a/evpp/EventLoop.h b/evpp/EventLoop.h index 5187cccff..fa8128efe 100644 --- a/evpp/EventLoop.h +++ b/evpp/EventLoop.h @@ -159,7 +159,7 @@ class EventLoop : public Status { // 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_options_t* opt = NULL) { + DnsID resolveDns(const char* host, DnsCallback cb, const hdns_setting_t* opt = NULL) { if (loop_ == NULL) return INVALID_DNS_ID; assertInLoopThread(); DnsID dnsID = generateDnsID(); diff --git a/evpp/TcpClient.h b/evpp/TcpClient.h index 2c7f7aed8..aaf667d00 100644 --- a/evpp/TcpClient.h +++ b/evpp/TcpClient.h @@ -156,7 +156,7 @@ class TcpClientEventLoopTmpl { // manages the underlying hdns_t lifetime, so this class only holds an id. int startResolveThenConnect() { cancelDnsQuery(); - hdns_options_t opt; + 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(), diff --git a/http/client/AsyncHttpClient.cpp b/http/client/AsyncHttpClient.cpp index f77f238a6..5cac31634 100644 --- a/http/client/AsyncHttpClient.cpp +++ b/http/client/AsyncHttpClient.cpp @@ -51,7 +51,7 @@ int AsyncHttpClient::doTask(const HttpClientTaskPtr& task) { return doTaskWithAddr(task, &peeraddr); } - hdns_options_t opt; + 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, diff --git a/unittest/hdns_benchmark.c b/unittest/hdns_benchmark.c index 6e2e61d9c..753497e8d 100644 --- a/unittest/hdns_benchmark.c +++ b/unittest/hdns_benchmark.c @@ -82,7 +82,7 @@ int main(int argc, char** argv) { 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_options_t opt; + hdns_setting_t opt; memset(&opt, 0, sizeof(opt)); opt.family = HDNS_QUERY_A; opt.timeout_ms = 5000; diff --git a/unittest/hdns_test.c b/unittest/hdns_test.c index 1a5fefac2..42ef67426 100644 --- a/unittest/hdns_test.c +++ b/unittest/hdns_test.c @@ -3,7 +3,7 @@ * * 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_options_t.nameserver. + * A records) and pointing the resolver at it via hdns_setting_t.nameserver. * * Covered: * 1. numeric IPv4 / IPv6 fast path @@ -139,7 +139,7 @@ static void on_expect(hdns_t* query, const hdns_result_t* result, void* userdata hloop_stop(e->loop); } -static void run_expect(hloop_t* loop, const char* host, const hdns_options_t* opt, +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)); @@ -171,7 +171,7 @@ int main() { snprintf(ns, sizeof(ns), "127.0.0.1:%d", g_mock_port); printf("mock nameserver on %s\n", ns); - hdns_options_t opt; + hdns_setting_t opt; memset(&opt, 0, sizeof(opt)); opt.family = HDNS_QUERY_A; // only A (mock answers A) opt.timeout_ms = 2000; @@ -184,7 +184,7 @@ int main() { // 2) numeric IPv6 fast path { - hdns_options_t o6 = opt; + hdns_setting_t o6 = opt; o6.family = HDNS_QUERY_BOTH; run_expect(loop, "::1", &o6, HDNS_STATUS_OK, 1); } @@ -192,7 +192,7 @@ int main() { // 3) /etc/hosts: localhost should resolve without touching the mock { int before = g_mock_queries; - hdns_options_t oh = opt; + 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 @@ -214,7 +214,7 @@ int main() { // 6) NXDOMAIN { - hdns_options_t onx = opt; + hdns_setting_t onx = opt; onx.use_cache = 0; run_expect(loop, "nx.test", &onx, HDNS_STATUS_NXDOMAIN, 0); } @@ -222,7 +222,7 @@ int main() { // 7) cancel before completion: callback must not fire { g_cancel_cb_called = 0; - hdns_options_t oc = opt; + 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. From 00cfcbe522a4032f15a7fb71c4449257c1405f4d Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 23 Jul 2026 19:51:12 +0800 Subject: [PATCH 11/17] refactor(examples): rename hdns_example -> host (async DNS lookup tool) Give the async DNS demo a short, purpose-obvious command name in the spirit of nc / curl / wget. `host` fits its behavior (resolve a name, print the addresses with concise output) better than `dig` (verbose, admin-oriented), and avoids colliding with the existing `nslookup` unittest (which exercises the legacy protocol/dns.c demo). - git mv examples/hdns_example.c -> examples/host.c; update its header/usage. - Makefile (EXAMPLES list + build rule), examples/CMakeLists.txt, docs. Builds and runs under Makefile and CMake. --- Makefile | 6 +++--- docs/cn/hdns.md | 2 +- examples/CMakeLists.txt | 6 +++--- examples/{hdns_example.c => host.c} | 0 4 files changed, 7 insertions(+), 7 deletions(-) rename examples/{hdns_example.c => host.c} (100%) diff --git a/Makefile b/Makefile index f6b86583f..2eafb2a15 100644 --- a/Makefile +++ b/Makefile @@ -69,7 +69,7 @@ EXAMPLES = hmain_test htimer_test hloop_test pipe_test \ udp_echo_server \ udp_proxy_server \ socks5_proxy_server \ - hdns_example \ + host \ multi-acceptor-processes \ multi-acceptor-threads \ one-acceptor-multi-workers \ @@ -173,8 +173,8 @@ udp_proxy_server: prepare socks5_proxy_server: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/socks5_proxy_server.c" -hdns_example: prepare - $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/hdns_example.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" diff --git a/docs/cn/hdns.md b/docs/cn/hdns.md index 66bbe72a6..831345a9b 100644 --- a/docs/cn/hdns.md +++ b/docs/cn/hdns.md @@ -142,7 +142,7 @@ int main() { } ``` -完整示例见 `examples/hdns_example.c`,性能对比见 `unittest/hdns_benchmark.c`。 +完整示例见 `examples/host.c`,性能对比见 `unittest/hdns_benchmark.c`。 ## C++ 层:EventLoop::resolveDns diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index a7a8f750e..324030d3f 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -12,7 +12,7 @@ list(APPEND EXAMPLES udp_echo_server udp_proxy_server socks5_proxy_server - hdns_example + host multi-acceptor-processes multi-acceptor-threads one-acceptor-multi-workers @@ -61,8 +61,8 @@ 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(hdns_example hdns_example.c) -target_link_libraries(hdns_example ${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/hdns_example.c b/examples/host.c similarity index 100% rename from examples/hdns_example.c rename to examples/host.c From cede86edbb8d0165b4f4382a8d6d5ff8a20f3bab Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 23 Jul 2026 20:23:42 +0800 Subject: [PATCH 12/17] fix(dns): address Copilot review comments on PR #855 - TcpClient::startResolveThenConnect: if resolveDns() fails to start on a hostname first-attempt (remote_addr unset), report a DNS failure via onDnsResolveFailed() instead of falling back to socket(0, ...) which fails with an unrelated socket error. - hdns_cancel(): add a re-entrancy guard (query->delivering) so an (API- forbidden) cancel from within the query's own callback fails safe instead of double-freeing; correct the misleading "cannot double-free" comment in hdns__deliver accordingly. - createsocket(): drop the misleading examples/protorpc reference in the return-value doc (that example returns the value as a connected fd); just state callers treat >=0 as success, <0 as failure. - hdns.h: fix stale @see examples/hdns_example.c -> examples/host.c. - hdns_test.c: replace the nonsensical hloop_new(FLAG == 0 ? 0 : 0) leftover with hloop_new(0). All DNS tests pass under Makefile and CMake. --- event/hdns.c | 13 +++++++++++-- event/hdns.h | 2 +- evpp/TcpClient.h | 13 ++++++++++--- unittest/hdns_test.c | 2 +- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/event/hdns.c b/event/hdns.c index 3e09515f8..7b457d37f 100644 --- a/event/hdns.c +++ b/event/hdns.c @@ -462,6 +462,7 @@ struct hdns_s { HEVENT_FIELDS // loop, event_type, event_id, cb, userdata, priority, flags... struct hdns_resolver_s* resolver; int detached; // 1 = cancelled: run to completion, drop result + int delivering; // 1 = inside hdns__deliver's callback (cancel guard) char host[HDNS_NAME_MAXLEN]; hdns_setting_t opt; hdns_cb dns_cb; // user callback (hevent_t::cb has a different type) @@ -666,12 +667,15 @@ static void hdns__deliver(hdns_t* q, int status) { // 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. The query is already removed from the in-flight list - // so a re-entrant hdns_cancel(q) from the callback cannot double-free. + // 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); @@ -988,6 +992,11 @@ hdns_t* hdns_resolve(hloop_t* loop, const char* host, 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; } diff --git a/event/hdns.h b/event/hdns.h index 4de0e5606..12fac606b 100644 --- a/event/hdns.h +++ b/event/hdns.h @@ -23,7 +23,7 @@ * - TTL-respecting per-loop cache (positive + negative). * - Cancelable query handle. * - * @see examples/hdns_example.c + * @see examples/host.c */ #include "hexport.h" diff --git a/evpp/TcpClient.h b/evpp/TcpClient.h index aaf667d00..615d1be32 100644 --- a/evpp/TcpClient.h +++ b/evpp/TcpClient.h @@ -60,8 +60,8 @@ class TcpClientEventLoopTmpl { // 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(). This matches the >0/=0/<0 convention used by - // other evpp clients (see examples/protorpc). + // 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) @@ -178,7 +178,14 @@ class TcpClientEventLoopTmpl { startConnectWithAddr(); }, &opt); if (dns_id == INVALID_DNS_ID) { - // could not start async resolve; fall back to synchronous path + // 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; diff --git a/unittest/hdns_test.c b/unittest/hdns_test.c index 42ef67426..5a92c267f 100644 --- a/unittest/hdns_test.c +++ b/unittest/hdns_test.c @@ -164,7 +164,7 @@ static void stop_after(htimer_t* timer) { } int main() { - hloop_t* loop = hloop_new(HLOOP_FLAG_QUIT_WHEN_NO_ACTIVE_EVENTS == 0 ? 0 : 0); + hloop_t* loop = hloop_new(0); hio_t* mock = start_mock_nameserver(loop); char ns[32]; From 079a60bd0f9f2f8e032ce727b395d3da1a5c2ed1 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 23 Jul 2026 20:26:42 +0800 Subject: [PATCH 13/17] refactor(hdns): pack detached/delivering as bitfields after HEVENT_FLAGS HEVENT_FIELDS ends with HEVENT_FLAGS (destroy/active/pending, each unsigned:1). Declare hdns_s's detached/delivering as unsigned:1 immediately after it so they share the same storage unit instead of using two separate ints, saving space. No behavior change. All DNS tests pass under Makefile and CMake. --- event/hdns.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/event/hdns.c b/event/hdns.c index 7b457d37f..642b449b6 100644 --- a/event/hdns.c +++ b/event/hdns.c @@ -460,9 +460,11 @@ typedef struct hdns_cache_entry_s { // 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; - int detached; // 1 = cancelled: run to completion, drop result - int delivering; // 1 = inside hdns__deliver's callback (cancel guard) char host[HDNS_NAME_MAXLEN]; hdns_setting_t opt; hdns_cb dns_cb; // user callback (hevent_t::cb has a different type) From d3a99a9abc21cc9cda7d1bb328ab7fde384c8c63 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 23 Jul 2026 20:39:19 +0800 Subject: [PATCH 14/17] fix(dns): collision-free txid allocation; clarify reconnect contract Address the second round of Copilot review comments on PR #855: - hdns.c: the 16-bit txid counter (next_txid += 2) could wrap and collide with a still-in-flight sub-query in a long-running loop, mis-delivering a response. Allocate the (base, base+1) pair by probing the (tiny) in-flight set and skipping any txid currently in use, so wrap can no longer alias a live query. - TcpClient.h: notifyDisconnectThenReconnect() calls startReconnect() (a member call) after the user onConnection callback, so destroying the client from within onConnection while reconnect is enabled would be a use-after-free. This is pre-existing, long-standing evpp onclose behavior (this method is a verbatim extraction of it), not introduced here; changing it to a lifetime-safe handle is out of scope for this PR. Make the comment state the contract honestly instead of over-claiming safety: disable reconnect (or defer destroy) before tearing down from onConnection. All DNS tests pass under Makefile and CMake. --- event/hdns.c | 40 ++++++++++++++++++++++++++++++++++++---- evpp/TcpClient.h | 12 ++++++++---- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/event/hdns.c b/event/hdns.c index 642b449b6..a0dfd7582 100644 --- a/event/hdns.c +++ b/event/hdns.c @@ -879,6 +879,38 @@ static void hdns__defer_complete(hdns_t* q) { } } +// Is @txid currently used by any in-flight (not-yet-done) sub-query? +static int hdns__txid_in_use(hdns_resolver_t* r, uint16_t 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 && q->sub[i].txid == txid) { + return 1; + } + } + } + return 0; +} + +// Allocate a (base, base+1) txid pair not currently in flight. Starts from a +// per-resolver monotonic counter and probes forward on collision. Bounded to +// avoid an infinite loop in the pathological case of >32K live sub-queries. +static uint16_t hdns__alloc_txid_pair(hdns_resolver_t* r) { + for (int tries = 0; tries < 0x8000; ++tries) { + uint16_t base = r->next_txid; + r->next_txid += 2; + if (!hdns__txid_in_use(r, base) && + !hdns__txid_in_use(r, (uint16_t)(base + 1))) { + return base; + } + } + // extremely unlikely fallback: return the current counter as-is. + uint16_t base = r->next_txid; + r->next_txid += 2; + return base; +} + hdns_t* hdns_resolve_ex(hloop_t* loop, const char* host, const hdns_setting_t* opt, hdns_cb cb, void* userdata) { @@ -965,10 +997,10 @@ hdns_t* hdns_resolve_ex(hloop_t* loop, const char* host, // 3) network query: assign sub-queries + txids, and start on the next loop // tick so this function returns the handle before any send/callback. - // Use a per-resolver monotonic counter (not random) so concurrent - // queries never share a txid and responses demux unambiguously. - uint16_t base = r->next_txid; - r->next_txid += 2; // A uses base, AAAA uses base+1 + // Allocate a (base, base+1) txid pair that is NOT currently used by any + // in-flight sub-query, so 16-bit wrap can never mis-deliver a response to + // a live query. The in-flight set is tiny, so a bounded probe is cheap. + uint16_t base = hdns__alloc_txid_pair(r); if (q->opt.family & HDNS_QUERY_A) { q->sub[0].qtype = HDNS_TYPE_A; q->sub[0].txid = base; diff --git a/evpp/TcpClient.h b/evpp/TcpClient.h index 615d1be32..063ff9755 100644 --- a/evpp/TcpClient.h +++ b/evpp/TcpClient.h @@ -208,10 +208,14 @@ class TcpClientEventLoopTmpl { } // @internal: notify a disconnect via onConnection, then reconnect if set. - // NOTE: onConnection is a user callback that may delete this object, so we - // snapshot whether to reconnect into a local BEFORE the callback and touch - // no members afterwards (a user destroying the object must first call - // setReconnect(NULL)). Shared by the onclose path and DNS-failure path. + // 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) { From 589c6c7a80ad5b3959f3daa8aecdf3f08d2e4ed7 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 23 Jul 2026 20:59:19 +0800 Subject: [PATCH 15/17] fix(dns): skip DNS for UDS targets; validate response source; simplify txid Third round of Copilot review on PR #855: - TcpClient::startConnect only runs async DNS when remote_port >= 0. A Unix Domain Socket target (remote_port < 0) carries a filesystem path in remote_host and is not an IP, so the previous !is_ipaddr() check wrongly routed it to DNS resolution and broke UDS clients; remote_addr is already set by createsocket() for UDS. - hdns__on_udp_read now validates the datagram source address (hio_peeraddr) against the nameserver the current attempt was sent to (new hdns_s.last_ns), dropping off-path spoofed/injected responses that guess the txid but not the source. Verified the mock-nameserver unit test and real-network resolves still pass (legitimate replies match). - Revert the earlier txid free-pair probing: it was over-defensive for a scenario (16-bit wrap colliding with a still-in-flight query) that requires tens of thousands of resolves on one loop with an old query still pending. Restore the simple monotonic counter and just document the theoretical bound. All DNS tests pass under Makefile and CMake. --- event/hdns.c | 55 +++++++++++++++++------------------------------- evpp/TcpClient.h | 5 ++++- 2 files changed, 23 insertions(+), 37 deletions(-) diff --git a/event/hdns.c b/event/hdns.c index a0dfd7582..c621085de 100644 --- a/event/hdns.c +++ b/event/hdns.c @@ -479,6 +479,7 @@ struct hdns_s { 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 @@ -755,6 +756,8 @@ static void hdns__send_queries(hdns_t* q) { 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; @@ -817,6 +820,8 @@ static void hdns__on_udp_read(hio_t* io, void* buf, int readbytes) { 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; @@ -826,6 +831,13 @@ static void hdns__on_udp_read(hio_t* io, void* buf, int readbytes) { 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. + if (src && sockaddr_compare((sockaddr_u*)src, &q->last_ns) != 0) { + return; + } + // parse this sub-query's answer uint32_t ttl = HDNS_CACHE_MAX_TTL; int rc = hdns__parse_response(pkt, readbytes, txid, @@ -879,38 +891,6 @@ static void hdns__defer_complete(hdns_t* q) { } } -// Is @txid currently used by any in-flight (not-yet-done) sub-query? -static int hdns__txid_in_use(hdns_resolver_t* r, uint16_t 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 && q->sub[i].txid == txid) { - return 1; - } - } - } - return 0; -} - -// Allocate a (base, base+1) txid pair not currently in flight. Starts from a -// per-resolver monotonic counter and probes forward on collision. Bounded to -// avoid an infinite loop in the pathological case of >32K live sub-queries. -static uint16_t hdns__alloc_txid_pair(hdns_resolver_t* r) { - for (int tries = 0; tries < 0x8000; ++tries) { - uint16_t base = r->next_txid; - r->next_txid += 2; - if (!hdns__txid_in_use(r, base) && - !hdns__txid_in_use(r, (uint16_t)(base + 1))) { - return base; - } - } - // extremely unlikely fallback: return the current counter as-is. - uint16_t base = r->next_txid; - r->next_txid += 2; - return base; -} - hdns_t* hdns_resolve_ex(hloop_t* loop, const char* host, const hdns_setting_t* opt, hdns_cb cb, void* userdata) { @@ -997,10 +977,13 @@ hdns_t* hdns_resolve_ex(hloop_t* loop, const char* host, // 3) network query: assign sub-queries + txids, and start on the next loop // tick so this function returns the handle before any send/callback. - // Allocate a (base, base+1) txid pair that is NOT currently used by any - // in-flight sub-query, so 16-bit wrap can never mis-deliver a response to - // a live query. The in-flight set is tiny, so a bounded probe is cheap. - uint16_t base = hdns__alloc_txid_pair(r); + // 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; diff --git a/evpp/TcpClient.h b/evpp/TcpClient.h index 063ff9755..1e2d61317 100644 --- a/evpp/TcpClient.h +++ b/evpp/TcpClient.h @@ -145,7 +145,10 @@ class TcpClientEventLoopTmpl { // Numeric-IP targets skip resolution and connect directly. // NOTE: any TcpClientTmpl subclass (WebSocketClient, ...) gets this for // free; no per-client DNS glue is needed. - if (!remote_host.empty() && !is_ipaddr(remote_host.c_str())) { + // 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(); From 2b75adfe034d8d60b1f40e29e87d8c94f42d1685 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 23 Jul 2026 21:20:46 +0800 Subject: [PATCH 16/17] fix(dns): keep scanning on source mismatch; honor never-reentrant guarantee Fourth round of Copilot review on PR #855: - hdns__on_udp_read: when a response's source address does not match the queried nameserver, `continue` to the next in-flight sub-query instead of `return`ing from the whole handler, so one rejected/spoofed datagram cannot prevent a legitimate match from being processed. - hdns__defer_complete and the network-start path previously fell back to synchronous completion / send if htimer_add failed, contradicting the "callback is NEVER invoked re-entrantly" guarantee documented in hdns.h. htimer_add(timeout_ms=1) never returns NULL, so that fallback was unreachable dead code; remove it and always defer, so the guarantee holds literally (an unreachable alloc failure leaves the query to be cleaned up at loop teardown). All DNS tests pass under Makefile and CMake. --- event/hdns.c | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/event/hdns.c b/event/hdns.c index c621085de..4e7d60885 100644 --- a/event/hdns.c +++ b/event/hdns.c @@ -833,9 +833,11 @@ static void hdns__on_udp_read(hio_t* io, void* buf, int readbytes) { // 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. + // 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) { - return; + continue; } // parse this sub-query's answer @@ -880,14 +882,15 @@ static void hdns__on_start(htimer_t* timer) { } static void hdns__defer_complete(hdns_t* q) { - // schedule completion on the next loop tick (1ms) so cb never runs - // re-entrantly inside hdns_resolve(). + // 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); - } else { - // extremely unlikely; deliver synchronously as a last resort - hdns__complete(q); } } @@ -992,12 +995,13 @@ hdns_t* hdns_resolve_ex(hloop_t* loop, const char* host, 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); - } else { - // extremely unlikely; start synchronously as a last resort - hdns__send_queries(q); } return q; } From 73aea45243b60464ba20ce71333b5c9506a5e2ad Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 23 Jul 2026 22:08:17 +0800 Subject: [PATCH 17/17] docs(dns): clarify CNAME and host example usage --- event/hdns.h | 5 ++++- examples/host.c | 10 +++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/event/hdns.h b/event/hdns.h index 12fac606b..8afaf0166 100644 --- a/event/hdns.h +++ b/event/hdns.h @@ -19,7 +19,10 @@ * - 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 and CNAME chain following. + * - 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. * diff --git a/examples/host.c b/examples/host.c index bffef3f0a..470828c23 100644 --- a/examples/host.c +++ b/examples/host.c @@ -1,12 +1,12 @@ /* - * hdns_example — asynchronous DNS resolve demo. + * host — asynchronous DNS resolve tool (like the system `host` command). * - * Usage: hdns_example [domain] [domain2 ...] - * e.g. hdns_example www.example.com github.com localhost 1.1.1.1 + * 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) and prints the resulting addresses. Quits when all queries - * have completed. + * getaddrinfo) via hdns_resolve() and prints the resulting addresses. Quits + * when all queries have completed. */ #include "hloop.h"