EMS 知识库
首页 / knowledge/代码解析/midware/ethernt/lan

dns.c

源文件路径:midware/ethernt/lan/dns.c 所属模块:midware/ethernt/lan(W5500 以太网协议栈应用层 — DHCP/DNS/Socket) 代码行数:591

1. 文件职责

本文件是 WIZnet 官方 DNS 客户端库(W5500 版)的实现,负责把云端服务器域名(如 cloudiot.alphaess.com)翻译成 IPv4 地址:构造 DNS 查询报文、通过 W5500 的 UDP socket 发往 53 端口、接收并解析应答,最终把 A 记录写回调用者缓冲区。

在 EMS 的数据流里它处在"DHCP 已拿到 IP,但 TCP 客户端还不能建链"的中间环节:ethernet.c 的 dns_pro_fun() 用它的输出 g_stNetSysInfo.Domain_IP 去 connect();解析失败时 dns_pro_fun() 会依次轮换 8 个公共 DNS 服务器重试。

与官方原版最大的差别是本工程把 DNS_run() 改造成了分次调用的非阻塞状态机(新增 static u8 s_u8_flag,注释为中文"先是发送查询指令"/"转到数据接收状态"),因为原版是"发送后在函数内忙等应答"的阻塞实现,会阻塞 task_ethernet。文件由 task_ethernet(优先级 17,0.5 秒一次)驱动收发包,超时用的秒计数由 task_system_aux(优先级 8)每秒递增。

2. 关键数据结构与宏

名称类型说明
INITRTT宏初始平滑往返时间 2000L(第 62 行);来自经典 BIND resolver 代码,本文件实际未使用
MAXCNAME宏CNAME 递归展开缓冲上限,值 MAX_DOMAIN_NAME + (MAX_DOMAIN_NAME>>1) = 168+84 = 252(第 63 行);parse_name()/dns_question()/dns_answer()/dns_makequery() 都在栈上开这么大的数组
TYPE_A … TYPE_TXT / TYPE_ANY宏DNS 资源记录类型:1 A、2 NS、5 CNAME、6 SOA、12 PTR、13 HINFO、15 MX、16 TXT、255 ANY 等(第 65-81 行)
CLASS_IN宏ARPA Internet 类 1(第 83 行);组帧时写死为 1,未用到该宏
AGAIN / LAGAIN / DGAIN / LDGAIN宏RTT 平滑增益参数 8 / 3 / 4 / 2(第 86-89 行);随 INITRTT 一起是 BIND 残留,本文件未使用
dhdr结构体DNS 报文头(第 92-115 行)。字段:id、qr、opcode、aa、tc、rd、ra、rcode(各自带 #define 取值)、qdcount、ancount、nscount、arcount。注意该结构体只是"解析结果的容器",其内存布局与线上 12 字节报文头并不一致(qs/rr 按位拆成了多个字节字段),所以代码用 get16(&msg[0]) 逐字段手工解析,而不是把报文直接映射到结构体上
QUERY / RESPONSE宏dhdr.qr 取值 0/1(结构体内定义,第 96-97 行)
IQUERY宏dhdr.opcode 取值 1(第 99 行)
NO_ERROR / FORMAT_ERROR / SERVER_FAIL / NAME_ERROR / NOT_IMPL / REFUSED宏dhdr.rcode 取值 0~5(第 105-110 行)

全局变量(第 118-122 行):

名称类型说明
pDNSMSGuint8_t*指向外部传入的收发共用报文缓冲区(本工程为 g_u8DNSBuf,512 字节)
DNS_SOCKETuint8_tDNS 使用的 W5500 socket 号,本工程为 SOCK_DNS=0
DNS_MSGIDuint16_tDNS 事务 ID,每次组帧自增
dns_1s_tickuint32_t超时用秒计数,由 DNS_time_handler() 递增

3. 函数清单

函数名作用域说明(一句话)
get16全局(第 125 行)从网络字节序缓冲读取一个 uint16_t
put16全局(第 134 行)把一个 uint16_t 以网络字节序写入缓冲,返回写后指针
parse_name全局(第 152 行)解析(含 0xC0 压缩指针间接寻址)的域名,展开为点分字符串并返回压缩字段总长度
dns_question全局(第 213 行)跳过 Question 段一条记录(域名 + type + class),返回下一条记录指针
dns_answer全局(第 239 行)跳过 Answer 段一条记录;若为 TYPE_A 则把 4 字节地址写入 ip_from_dns
parseDNSMSG全局(第 334 行)解析完整 DNS 应答:还原 12 字节报文头到 dhdr,遍历 Question/Answer 段,按 rcode 返回 1/0/-1
dns_makequery全局(第 412 行)组 DNS 查询报文(报文头 + 域名点分段 + type/class),返回报文长度
check_DNS_timeout全局(第 473 行)基于 dns_1s_tick 与内部静态 retry_count 判断是否超时,返回 -1/0/1
DNS_init全局(第 494 行)绑定 socket、缓冲区与初始报文 ID
DNS_run全局(第 502 行)非阻塞 DNS 状态机:状态 1 发查询、状态 2 收应答并解析
DNS_time_handler全局(第 585 行)秒计数 dns_1s_tick++

文件内没有 static 函数;check_DNS_timeout() 内含一个 static uint8_t retry_count,DNS_run() 内含一个 static u8 s_u8_flag。

4. 核心逻辑解析

4.1 报文格式与组帧(dns_makequery(),第 412-462 行)

输出报文(全部字段按网络字节序,put16 写入):

偏移 0-1    id      = ++DNS_MSGID(初值 DNS_MSG_ID=0x1122)
偏移 2-3    标志    = (op << 11) | 0x0100    // op=0 → 0x0100,QR=0 查询、RD=1 期望递归
偏移 4-5    qdcount = 1
偏移 6-7    ancount = 0
偏移 8-9    nscount = 0
偏移 10-11  arcount = 0
偏移 12..   QNAME   = 域名按 '.' 拆成 [len][label]... 形式,末尾 0x00
之后 2 字节  QTYPE  = 0x0001 (TYPE_A)
之后 2 字节  QCLASS = 0x0001 (CLASS_IN)

域名的拆分段逻辑(第 435-456 行):先把 name 用 strcpy 拷到栈上的 sname[MAXCNAME],然后用 strchr 逐段找点,每段写"长度 + 内容",最后补 0x00 结束。返回值为 cp - buf,即整包长度。

注意函数参数 len(调用方传入 MAX_DNS_BUF_SIZE=512)在第 440 行被重新赋值为当前段的长度,原始上限从此丢失:整个函数既没有校验 strlen(name) < MAXCNAME,也没有校验输出是否超出 512,是典型的 BIND 老代码直译风格。

4.2 非阻塞状态机(DNS_run(),第 502-581 行)

static u8 s_u8_flag 只有两个取值,跨调用保持:

因此 DNS_run() 需要被反复调用才能完成一次解析,调用方每 0.5 秒调一次(ethernet.c:424 的 > 500 tick 闸门),单次解析的实际耗时约 DNS_WAIT_TIME(3 秒) ~ 3.5 秒。

4.3 超时判定(check_DNS_timeout(),第 473-489 行)

static uint8_t retry_count;                 // 函数内静态,跨调用保持
if (dns_1s_tick >= DNS_WAIT_TIME) {         // 3 秒到
    if (retry_count >= MAX_DNS_RETRY) {     // 已重试 1 次
        retry_count = 0;
        return -1;                          // 判定超时
    }
    retry_count++;
    return 0;                               // 计时到,但不算超时
}
return 1;                                   // 计时未到

关键点:dns_1s_tick 只在 case 1 发查询时清零(第 528 行),超时分支不重置也不重发——MAX_DNS_RETRY = 1,而 retry_count 初值 0,语义上"允许 1 次额外等待 3 秒",但并没有真正重发一次查询。等效行为是:发包后大约 3 秒内没收到应答,下一次调用即判定失败(总等待时间约 3~3.5 秒)。这与头文件注释"max time = MAX_DNS_RETRY * DNS_WAIT_TIME"以及"Requery Count"的描述不一致。

4.4 应答解析

报文头还原(parseDNSMSG(),第 334-399 行):先把 dhdr 整体 memset 清零,再逐字段填:

Question 段:循环 qdcount 次调用 dns_question(),每次 parse_name() 展开域名并 +2(type)+2(class)推进;parse_name() 返回 -1 则整体返回 -1。

Answer 段:循环 ancount 次调用 dns_answer():

最后按 rcode 返回:NO_ERROR → 1,其它 → 0。nscount 与 arcount 两个循环体内是空语句(第 386-395 行),只做计数占位,未真正解析授权段与附加段。

域名展开(parse_name(),第 152-203 行):处理 0xC0 压缩指针((slen & 0xc0) == 0xc0 时用 ((slen & 0x3f)<<8) + 下一字节 作偏移回跳,且只把首个指针算进 clen),逐段写出并以 . 连接,最后补 '\0';根名(nseg == 0)输出单个 .。缓冲区不足时(len < 0)返回 -1。

4.5 与上层(ethernet.c)的时序配合

ethernet.c 的 dns_pro_fun()(第 417-540 行)是唯一调用方:

秒计数 dns_1s_tick 由 ethernet.c:1087 在 task_system_aux 中每秒递增,与运行 DNS_run() 的 task_ethernet 不是同一任务,dns_1s_tick 无 volatile 修饰也无保护。

5. 对外接口

本文件导出的函数(声明见 dns.h):DNS_init、DNS_run、DNS_time_handler。

另外以下函数虽未在头文件声明,但因未加 static 而具有全局链接属性:get16、put16、parse_name、dns_question、dns_answer、parseDNSMSG、dns_makequery、check_DNS_timeout。

本文件导出的全局变量:pDNSMSG、DNS_SOCKET、DNS_MSGID、dns_1s_tick。

本文件调用的外部函数/变量:

6. 依赖关系

7. 备注与风险点

  1. dns_makequery() 的域名长度与越界风险(第 432 行 strcpy(sname, name)):sname[MAXCNAME] 只有 252 字节,name 来自可配置的服务器域名,长度完全不受控;同时组帧输出也未与 MAX_DNS_BUF_SIZE(512) 比较,超长域名会造成栈溢出或缓冲区溢出。头文件对此的说明是"调用者应保证域名 +1 ≤ 168",但没有代码强制。
  2. dns_makequery() 的 len 形参被覆盖(第 440 行):头注释声明 len 是"缓冲区最大尺寸",实现中第一次循环就把它改成当前段长度,原语义丢失,属官方遗留写法,极易误用。
  3. parseDNSMSG() 不校验报文的 id 与源 IP:任何到达 socket 0 的 UDP 报文(recvfrom 取回的 ip/port 从未比对)都会被当作 DNS 应答解析,存在被伪造应答篡改解析结果的可能;对 EMS 这种长期在线的设备值得关注。
  4. parseDNSMSG() 没有长度边界检查:dns_question()/dns_answer() 依据报文内的 qdcount/ancount 与各记录的长度字段推进指针,若应答被截断(例如 len > MAX_DNS_BUF_SIZE 被截到 512,第 537-538 行)或 ancount 被伪造,可能越界读取缓冲区之后的内存。default 分支不推进指针也会导致后续记录解析错位。
  5. 超时重试的语义与文档不符:MAX_DNS_RETRY = 1 名义上是"重查询 1 次",实际实现中不会重发查询(dns_1s_tick 不清零、s_u8_flag 不回退到 1),只是多给 3 秒等待;dns.h 的"max time = MAX_DNS_RETRY * DNS_WAIT_TIME"注释不准确。
  6. check_DNS_timeout() 的静态 retry_count 不会在解析成功时复位:成功路径(case 2 收到包)没有清它,但由于只在超时分支递增、在判定超时时清零,实践中影响有限;不过它与 DNS_run() 的 s_u8_flag 共同构成了两处跨调用隐藏状态,DNS_run() 因此不可重入,同时只能有一个域名查询在途(本工程只有一条查询,暂无问题)。
  7. dns_1s_tick 无 volatile 也无临界区:由 task_system_aux 每秒写、由 task_ethernet 读,编译器优化下存在读到陈旧值的理论风险(对比 dhcp.c 中同作用的 dhcp_tick_1s 已加 volatile)。
  8. nscount/arcount 循环体为空(第 386-395 行):授权段与附加段完全不解析,也在返回值判断里不起作用;这是官方为省代码量的裁剪,但会让阅读者误以为已处理。
  9. 死代码与 BIND 残留:INITRTT、AGAIN、LAGAIN、DGAIN、LDGAIN、CLASS_IN、TYPE_MD/MF/MB/MG/MR/NULL/WKS/PTR(部分仅在 switch 中被列举)以及 <stdlib.h> 的 include 都是未使用的残留;#ifdef _DNS_DEUBG_(第 369、379 行)拼写错误,且其块内的 printf 缺少右括号与分号,一旦该宏被定义将无法编译。
  10. DNS_run() 中 socket() 每次都执行(第 520 行,case 1):socket() 内部会先 close() 再打开(socket.c:141),意味着每次重新发起解析都会重建 socket 0;socket 0 同时被约定为 DNS 专用,若其它模块误用会互相踢掉。
  11. getSn_RX_RSR() 截断:if(len > MAX_DNS_BUF_SIZE) len = MAX_DNS_BUF_SIZE; 之后 recvfrom() 只收 512 字节,而 W5500 的 RX 缓冲区里剩余数据会滞留在 socket 中;好在此后立即 close(),影响被限制在单次查询内。