教程 24:从 mdns_example_init() 到 mdns_recv()——mDNS、DNS-SD、Probe/Announce 与 PTR/SRV/TXT 服务发现
摘要:从 upstream mDNS example 追踪 Probe/Announce、PTR/SRV/TXT 与 Query/Response,并用真实 48 帧抓包将源码和线上报文逐项互证。
[TOC]
mDNS(Multicast DNS,多播 DNS)把 DNS 风格的名称解析限制在本地链路:querier(查询端)和 responder(响应端)通过 UDP 5353 向 IPv4 224.0.0.251 或 IPv6 FF02::FB 交换 DNS 格式的 Query/Response,而不是先把 .local. 名称交给传统递归 DNS server。S3 DNS-SD(DNS-Based Service Discovery,基于 DNS 的服务发现)进一步约定如何用 DNS Resource Record(RR,资源记录)描述“有哪些服务、服务实例叫什么、目标主机和端口是什么、还有哪些附加属性”。S4
标题中的几个协议动作先建立最小语义。Probe 是 responder 在正式宣告要求单一所有者的 unique name/record 前发送的冲突探测;Announcement 是名称确认可用后主动发送的 unsolicited response(非请求触发响应),用于让同链路 peer 尽快学习当前记录。S3 DNS-SD 中,PTR 把 service type 指向 service instance,SRV 给出该实例的 target host 与 port,TXT 承载服务附加元数据,A/AAAA 分别给出 target host 的 IPv4/IPv6 地址。S4 因而一次完整服务发现不是“查到一个 IP”就结束,而是从 service type 逐步得到 instance、target/port、metadata 与 address。
Stage 14 已经建立传统 DNS resolver。Stage 24 只恢复当前需要的差异:mDNS 是本地链路上的 multicast name resolution,DNS-SD 是建立在 DNS record 之上的 service discovery 约定;本文随后从真实 mdns_example_init() 入口继续追踪 Probe/Announce、Query/Response、PTR/SRV/TXT/A/AAAA 与真实 PCAP。S1S2S6
阅读源码前:建议提前阅读
这些资料用于规范核对和进一步阅读,不是理解正文的强制前置条件:
- Apple Bonjour Concepts —— Bonjour 是 Apple 对这套零配置网络能力的总称;本资料用于建立 Bonjour、mDNS 与 DNS-SD 的整体关系,尤其关注 local-link name 与 service discovery 的区别。S7
- RFC 6762:Multicast DNS —— 用于核对 UDP 5353、multicast destination、Probe/Announcement 与 Query/Response policy。S3
- RFC 6763:DNS-Based Service Discovery —— 用于核对 service instance naming,以及 PTR/SRV/TXT/A/AAAA 如何组成 service discovery 结果。S4
进入源码前先看完整协议流程
当前 example 同时包含“名称唯一性建立”和“服务/主机查询响应”两类动作。mDNS Query 还可以携带 Known Answer(已知答案):querier 把自己已经缓存的 answer 放进 Query,responder 若判断这些记录仍足够新鲜,就可以抑制重复发送,从而减少本地链路上的 multicast 流量。S3 按本文实际会进入源码的路径,可以先把它们串成一条总流程:
1 | flowchart TD |
Known Answer 会在真正进入 mdns_handle_question() 时映射到 parser 与 reply bitmap。这里的 reply bitmap 不是 mDNS 线上字段,而是 lwIP 内部用来记录“本次 Response 应包含哪些 host/service records”的选择位图。
协议动作与本文源码落点如下:
| 协议阶段 | 协议对象/动作 | lwIP 实现入口 | 关键对象/状态 | 下一步 |
|---|---|---|---|---|
| 建立 transport | UDP 5353、mDNS multicast | mdns_resp_init() |
global mdns_pcb |
注册 RX callback |
| 注册本接口 | hostname + multicast membership | mdns_resp_add_netif() |
per-netif struct mdns_host |
进入 Probe |
| 唯一性探测 | Probe | mdns_resp_restart() → mdns_probe_and_announce() |
MDNS_STATE_PROBE_* |
Announce 或 conflict/restart |
| 发布服务 | service instance / PTR / SRV / TXT | mdns_resp_add_service() |
struct mdns_service |
重新 Probe/Announce |
| 接收查询 | Query + Known Answer | mdns_recv() → mdns_handle_question() |
parser state + reply bitmap | 选择 records |
| 构造响应 | A/AAAA/PTR/SRV/TXT | mdns_create_outpacket() |
output packet builder | mdns_send_outpacket() |
后面的 Source-driven 主线仍按真实执行顺序展开;上面的协议模型只负责告诉读者“当前函数正在实现哪一个协议动作”。
1. 当前 example 的第一条真实执行链
contrib/examples/mdns/mdns_example.c 给出最小完整入口:S2
1 | void |
它完成四件事:
- 初始化全局 mDNS UDP transport;
- 把
netif_default注册成 hostnamelwip.local的 responder; - 发布一个
myweb._http._tcp.local服务; - 请求发送当前已知记录的 unsolicited announcement。
当前 contrib/examples/example_app/lwipopts.h 把 LWIP_MDNS_RESPONDER 定义为 LWIP_UDP,因此 responder Core 会随 UDP 编译;但当前 lwipcfg.h 默认 LWIP_MDNS_APP=0,test.c 不会自动调用 mdns_example_init()。S2
所以“代码已编译”和“example 已运行”是两回事。
2. mdns_resp_init():一个双栈 UDP PCB 绑定 5353
进入 mdns_resp_init():S1
1 | void |
关键运行对象是:
1 | one mdns_pcb |
RFC 6762 指定 mDNS 使用 UDP 5353,IPv4 multicast address 为 224.0.0.251,IPv6 等价地址为 FF02::FB。S3
3. mdns_resp_add_netif():真正把 responder 绑定到一张接口
全局 PCB 建好后,mdns_resp_add_netif(netif, hostname) 创建 per-netif struct mdns_host,把 hostname 保存到 netif client data,并准备 IPv4/IPv6 multicast destination。S1
当前函数还会显式加入两类 multicast group:
1 |
|
v4group 与 v6group 来自:
1 | 224.0.0.251 |
因此 Stage 17 的 IGMP 与 Stage 15 概览中的 MLD 不是独立知识点,它们在这里变成 mDNS responder 的 transport 前置条件。
4. struct mdns_host 是 per-netif 状态机,不是全局 hostname table
每个已注册 netif 有自己的 struct mdns_host:S1
1 | typedef enum { |
对象还保存:
1 | hostname |
这说明 mDNS responder 不是“收到 query 就永远直接答”;启动前要先通过 probing 确认 unique name 不冲突。
5. mdns_resp_add_netif() 最后不是 announce,而是 restart probing
函数结尾调用:
1 | mdns_resp_restart(netif); |
mdns_resp_restart() 再进入 mdns_resp_restart_delay(),把状态设为 MDNS_STATE_PROBE_WAIT,通过 sys_timeout() 安排后续 mdns_probe_and_announce()。S1
所以真实初始化链是:
1 | flowchart TD |
6. mdns_probe_and_announce():Probe/Announce 状态机的真实推进器
这个 timer callback 是当前 responder 生命周期的核心:S1
1 | static void |
probe 阶段会对 IPv4/IPv6 都发送 probe;IPv4 只有在接口已有非零地址时才参与。
7. Probe 成功后才进入 Announce
继续阅读同一个 mdns_probe_and_announce() 的 announce 分支:S1
1 | case MDNS_STATE_ANNOUNCE_WAIT: |
probe/announce 生命周期可以画成:
1 | stateDiagram-v2 |
这个状态机解释了为什么 service/hostname 变更需要 restart,而不是只更新一个字符串。
8. Probe 冲突语义如何落到 lwIP 的状态与限速字段
mDNS 对 unique record 做 Probe 的目的,是在把名称视为“本机拥有”之前发现同链路冲突;如果收到与候选 unique record 冲突的响应,responder 不能继续把该名称当成已确认可用。RFC 6762 还规定了冲突持续发生时的速率限制,避免设备在冲突环境中形成高频 Probe 风暴。S3 lwIP 把这些协议语义落在 struct mdns_host 的 conflict timestamps、conflict count、rate-limit flag 和 MDNS_STATE_* 迁移中,并由 mdns_probe_and_announce() 继续推进。S1
Stage 25 的 IPv4 ACD 也会出现“投入使用前先检测冲突”,但它验证的是 IPv4 address ownership;这里验证的是 mDNS name/resource-record ownership。两条实现链不能混为同一个状态机。
9. DNS-SD real example:myweb._http._tcp.local
继续阅读 mdns_example_init() 中注册 DNS-SD service 的调用:S2
1 | mdns_resp_add_service(netif_default, |
mdns_resp_add_service() 创建 struct mdns_service,把 example 的 instance、service type、transport、port 与 TXT callback 保存为后续 packet builder 的输入:S1S2
1 | myweb + _http + _tcp + 80 + srv_txt |
myweb._http._tcp.local 可以拆成 service instance myweb、service type _http、transport _tcp 与 local domain。浏览 _http._tcp.local 时,PTR 把 service type 指向这个 instance;SRV 再给出 target host 与 port 80;TXT 给出 path=/ 等 metadata;最后 A/AAAA 把 target host 映射到 IP 地址。S4S7 下面继续看这些协议对象怎样进入 struct mdns_service 与 serializer。
10. mdns_resp_add_service() 会触发重新 Probe
完整函数在保存 service 后执行:
1 | mdns->services[slot] = srv; |
这不是多余动作。新增 service 会改变当前 responder 要宣告的 unique records,因此需要重新走 probing/announcing lifecycle。S1
11. TXT 不是固定字符串字段,而是在生成 Reply 时调用 callback
example 的 TXT callback 是:S2
1 | static void |
mdns_resp_add_service() 保存 txt_fn,真正构造 TXT RR 时 callback 可动态填数据。这使 service metadata 不必在注册时永久固定。
mdns_resp_add_service_txtitem() 把每个 TXT item 以 DNS label-style length prefix 写入 service 的 TXT buffer:S1
1 | err_t |
12. DNS-SD 记录关系如何映射到 lwIP 对象与 serializer
把前面的协议模型重新挂回源码,可以得到一条直接的对象映射:service type 的 PTR 用于发现 instance,instance 的 SRV 给出 target/port,TXT 给出 metadata,target host 的 A/AAAA 给出地址。S4 当前 lwIP 分别用 service object、reply bitmap 和 serializer 实现这些动作:
| DNS-SD 角色 | lwIP 当前实现对象/路径 |
|---|---|
| service instance/type/transport/port | struct mdns_service |
| TXT 动态内容 | txt_fn → mdns_resp_add_service_txtitem() |
| service browse/instance reply bitmap | check_service() / serv_replies[] |
| PTR 序列化 | mdns_add_servicetype_ptr_answer() / mdns_add_servicename_ptr_answer() |
| SRV 序列化 | mdns_add_srv_answer() |
| TXT 序列化 | mdns_add_txt_answer() |
| host address additional records | mdns_add_a_answer() / IPv6 对应 builder |
因此后文看到 PTR/SRV/TXT 时,会同时保持两条线:协议上该 record 在 service discovery 中承担什么角色,以及当前 Query 如何把 reply bitmap 置位、mdns_create_outpacket() 最终调用哪个 serializer。S1
13. 把当前 responder 跑起来:先 configure,再追加 Stage 24 配置
前 12 节已经得到一个可以明确观测的结论:example 会注册 lwip.local,发布 myweb._http._tcp.local,并在完成 probing 后发送 announcement。此时再引入 Host 工具,目的是验证这些对象是否真的出现在链路上,而不是把 tcpdump 当成脱离源码的工具教程。
本仓库的 scripts/configure-debug.sh 每次都会从 upstream lwipcfg.h.example 刷新本地 lwipcfg.h;因此 Stage 专属 override 必须放在 configure 之后,然后直接 build,不能再执行一次 configure。S5
Ubuntu Host 若还没有 Avahi 查询工具,先补充:
1 | sudo apt update |
从仓库根目录执行:
1 | scripts/configure-debug.sh |
然后追加 Stage 24 的 IPv4/mDNS 配置:S2S5
1 | cat >> upstream/lwip/contrib/examples/example_app/lwipcfg.h <<'STAGE24_CFG' |
这里真正用于启动 mDNS example 的专属开关是 LWIP_MDNS_APP=1。当前 contrib/examples/example_app/lwipopts.h 已经把 LWIP_MDNS_RESPONDER 绑定到 LWIP_UDP,而 lwipcfg.h.example 默认把 LWIP_MDNS_APP 关闭,因此“responder 源码被编译”与“example_app 运行时真正调用 mdns_example_init()”是两个不同条件。S2
这次实验同时关闭 DHCP/AutoIP,并固定 198.18.0.200/24,是为了把 mDNS/IGMP 流量从 DHCP、ACD 等其他阶段流量中分离出来;这只是本 Host 实验配置,不是 mDNS 协议要求。S5
直接构建 example,不再 configure:S5
1 | scripts/build.sh example |
生成程序:
1 | build/example/contrib/ports/unix/example_app/example_app |
14. 建立 lwip0 TAP,并在启动 example 之前先开始抓包
Stage 2 起的 Host 实验网络固定为:S5
1 | Linux Host : 198.18.0.1/24 |
若需要重新创建 TAP:
1 | sudo ip link del lwip0 2>/dev/null || true |
确认 host route:
1 | ip -4 addr show lwip0 |
必须先抓包、再启动 example。Probe 和 Announcement 都发生在 responder startup lifecycle,如果反过来执行会直接丢掉本文最重要的一组启动证据。
1 | mkdir -p captures |
这里显式使用 ip,本轮先只研究 IPv4 mDNS 与 IGMP;lwIP binary 仍然有 IPv6 link-local address,但 IPv6 mDNS/MLD 不进入当前 capture filter。
15. 启动 example_app:运行日志已经证明 probing 成功
另一个终端启动 lwIP:S5
1 | PRECONFIGURED_TAPIF=lwip0 \ |
本次真实运行日志是:S6
1 | Starting lwIP, local interface IP is 198.18.0.200 |
mdns_example.c 的 mdns_example_report() 只是把 callback 参数打印出来;src/include/lwip/apps/mdns.h 当前定义 MDNS_PROBING_CONFLICT=0、MDNS_PROBING_SUCCESSFUL=1,因此最后一行表示当前 name/service probing 最终成功。S1S2S6
需要注意实际日志是 netif 1,不是预先假定的 netif 0。文章只使用当前运行结果,不把 netif number 写成 mDNS 固定属性。
16. 真实 PCAP 前 22 帧:IGMP、Probe 和两次 Announcement 都出现了
本次 tcpdump 最终报告:S6
1 | 48 packets captured |
PCAP 覆盖约 52.291553 s。其中最能建立启动心智模型的帧如下:S6
| Frame | 相对时间 | 方向 | 关键内容 |
|---|---|---|---|
| 1 | 0.000000 s | 198.18.0.200 → 224.0.0.251 |
IGMP type 0x16,加入 mDNS IPv4 group;Ethernet 目的 MAC 01:00:5e:00:00:fb |
| 2 | 0.238883 s | 198.18.0.200:5353 → 224.0.0.251:5353 |
Probe-style Query:lwip.local ANY、myweb._http._tcp.local ANY;Authority 带 A 与 SRV |
| 13 | 2.129987 s | 同上 | 后续 Probe 已同时带 A、AAAA 与 SRV authority data |
| 20 | 2.880903 s | 198.18.0.200:5353 → 224.0.0.251:5353 |
Announcement,Response/AA,8 个 Answer |
| 22 | 3.880414 s | 同上 | 第二次同类 Announcement,与 Frame 20 相隔约 0.999511 s |
当前 capture 不是一条干净的“只有 3 个 Probe”序列。Frame 2、4~10、13、15、19 都呈现 Probe-style ANY Question + Authority RR;这与当前 responder 可以被 netif/service/address 状态变化重新 mdns_resp_restart() 的实现相容,但仅凭 PCAP 不能唯一判断每次 restart 的具体触发者,因此正文不把这些帧强行归因为某一个 callback。S1S6
17. Frame 20/22 把 DNS-SD 的 PTR/SRV/TXT 模型直接放到了线上
Frame 20 与 Frame 22 都是从 lwIP 发往 224.0.0.251:5353 的 authoritative Response;每帧包含 8 个 Answer。S6
Frame 20 的实际记录是:S6
| RR | Name | RDATA / 含义 |
|---|---|---|
| A | lwip.local |
198.18.0.200 |
| PTR | 200.0.18.198.in-addr.arpa |
lwip.local |
| AAAA | lwip.local |
fe80::12:34ff:fe56:78ab |
| PTR | IPv6 reverse name | lwip.local |
| PTR | _services._dns-sd._udp.local |
_http._tcp.local |
| PTR | _http._tcp.local |
myweb._http._tcp.local |
| SRV | myweb._http._tcp.local |
priority 0、weight 0、port 80、target lwip.local |
| TXT | myweb._http._tcp.local |
path=/ |
Frame 20/22 把第 9~13 节的概念直接变成 packet evidence:
1 | flowchart LR |
这里还有一个容易忽略的边界:example 宣告 _http._tcp port 80,并不代表当前 binary 一定启用了 HTTPD。mdns_resp_add_service() 发布的是服务发现记录;业务 server 是否真的在监听该端口是另一层应用责任。S2
18. 用 avahi-resolve 生成一个真实 Host Query
Host 上执行:S6
1 | avahi-resolve -4 -n lwip.local |
本次实际返回:S6
1 | lwip.local 198.18.0.200 |
对应 PCAP 的 Frame 46/47:S6
| Frame | 相对时间 | 方向 | DNS 内容 |
|---|---|---|---|
| 46 | 48.617123 s | 198.18.0.1:5353 → 224.0.0.251:5353 |
Query:lwip.local A,没有 QU bit |
| 47 | 48.617208 s | 198.18.0.200:5353 → 224.0.0.251:5353 |
Response:Answer=lwip.local A 198.18.0.200;Additional=lwip.local AAAA fe80::12:34ff:fe56:78ab |
在这个 TAP capture 里,两帧时间差约 85 µs。它只能说明当前同机 Host/TAP 实验里的处理时序,不能泛化成真实 Ethernet 网络的 mDNS RTT。S6
Frame 46 没有请求 QU,因此 Frame 47 仍然发往 224.0.0.251 multicast,而不是直接 unicast 回 198.18.0.1。接下来正好从这一个真实 Query 回到源码输入路径。
19. Frame 46 怎么进入 mdns_recv():UDP 5353 callback 恢复接收接口和 DNS parser 状态
第 2 节已经看到 mdns_resp_init() 执行 udp_recv(mdns_pcb, mdns_recv, NULL)。因此 Frame 46 被 UDP 层匹配到本地 5353 PCB 后,当前 callback 就是 mdns_recv()。S1S6
进入 mdns_recv():S1
1 | static void |
这里先完成三件事:
ip_current_input_netif()取得真正收到这个 UDP datagram 的接口;NETIF_TO_HOST()确认这张接口已经注册 mDNS responder;- 从
pbuf复制固定 DNS header,并把 parser offset 移到 Question section 起点。
接着同一个函数把 DNS section count 保存为可递减的 parser state:S1
1 | memset(&packet, 0, sizeof(packet)); |
Frame 46 的 qd=1, an=0, ns=0, ar=0 因此会变成一个只有一个 Question 的 struct mdns_packet。S6
20. mdns_recv() 根据 QR bit 把 Frame 46 送进 mdns_handle_question()
继续阅读 mdns_recv() 的主分流。下面是按 Frame 46 当前条件裁剪的执行路径阅读版:省略了当前 answers=0 不会命中的 pending Known-Answer continuation 分支,保留 Response / TC / 普通 Query 的原执行顺序。S1
1 | if (hdr.flags1 & DNS_FLAG1_RESPONSE) { |
Frame 46 的 QR=0、TC=0,所以不会进入 Response handler,也不会进入 truncated-query timer;它直接调用 mdns_handle_question()。这就是抓包和函数分发之间的第一条直接对应关系。S1S6
21. mdns_handle_question():先解析 Question,再用 Known Answer 和 Authority 修正 reply bitmap
进入 mdns_handle_question() 后,responder 只有在 ANNOUNCING/COMPLETE 阶段才回答普通 Query。下面是按 Frame 46 当前条件裁剪的执行路径阅读版:省略 probing tiebreaking 与 pkt->next_answer 扩展 Known-Answer 分支,只保留当前普通 Query 实际经过的三个 parser。S1
1 | memset(&reply, 0, sizeof(struct mdns_outmsg)); |
这三个函数分别承担不同语义:
1 | Question |
Frame 46 没有 Answer/Authority,因此后两步不会改变 reply bitmap;真正决定它需要 A Reply 的是 Question parser。
22. mdns_parse_pkt_questions():lwip.local A 最终变成 REPLY_HOST_A
继续进入 mdns_parse_pkt_questions():S1
1 | static err_t |
Frame 46 的 name=lwip.local、type=A,所以 check_host() 把 REPLY_HOST_A 置入 reply.host_replies。它的 class 没有 QU bit,因此 q.unicast=0,unicast_reply_requested 仍然为 0。S1S6
对于 DNS-SD browse,则同一个函数会让 _http._tcp.local PTR 经过 check_service() 变成 service reply bitmap;也就是说 hostname resolution 和 service discovery 共用同一 parser,只是匹配对象不同。
23. Known Answer suppression:为什么 mDNS Query 可以让 responder 少发一些记录
Frame 46 没有 Known Answer,但当前实现允许 querier 把已经缓存的 Answer 放在 Query 中。mdns_parse_pkt_known_answers() 会把“本来准备发、而且对端已经拥有且 TTL 仍足够”的 RR 从 reply bitmap 清掉。S1S3
以 A record 为例。下面是按 A record 条件裁剪的执行路径阅读版:省略 PTR/AAAA/service Known Answer 分支,只保留 A RR 的匹配、TTL 与 payload 比较。S1
1 | match = reply->host_replies & check_host(netif, &ans.info, &rev_v6); |
因此“Known Answer suppression”不是发送后再丢包,而是在 packet builder 之前直接修改 reply bitmap。
本次 PCAP 没有捕获到针对 lwip.local 的 Known Answer Query,所以这里属于源码/规范机制说明,不伪写成本次实验已经验证的现象。S1S3S6
24. 回到 mdns_handle_question():Frame 46 为什么不随机延迟、也不转成 unicast
三个 parser 返回后,mdns_handle_question() 先汇总是否真的有 RR 要发送:S1
1 | rrs_to_send = reply.host_replies | reply.host_questions; |
回到 mdns_handle_question() 的 delay decision:当前 Query 只有一个 lwip.local A,属于 single unique answer,而且不是 Probe,因此 delay_response 会被清零。S1
1 | if (((pkt->questions == 1) && (!shared_answer) && !reply.probe_query_recv) |
这与 Frame 46/47 在 TAP 上只相差约 85 µs 的实验事实一致;但该时间只验证“当前路径没有走随机 delay timer”,不是对真实网络 latency 的性能承诺。S1S6
另一方面,Frame 46 的 Question 没有 QU bit,source port 又是标准 5353,也不是 direct-unicast/legacy query,因此 send_unicast 不成立,最终 Response destination 仍是 mDNS multicast group。Frame 47 的目的地址确实是 224.0.0.251。S1S6
25. mdns_create_outpacket():Frame 47 为什么 Answer 是 A,而 Additional 又带一个 AAAA
response policy 确定后,mdns_send_outpacket() 首先调用 mdns_create_outpacket()。后者根据 reply bitmap 写 Answer section;REPLY_HOST_A 会进入 mdns_add_a_answer():S1
1 |
|
继续阅读 mdns_create_outpacket() 的 service answer loop,service reply bitmap 分别落到 PTR/SRV/TXT serializer:S1
1 | if (msg->serv_replies[i] & REPLY_SERVICE_TYPE_PTR) { |
随后 Additional-RR logic 会在请求 host address 或 service instance/SRV 时补齐其他有效 host address。当前 netif 同时有有效 IPv6 link-local address,因此 Frame 47 在 A Answer 之外又出现:S1S6
1 | Additional: |
这与启动日志中的 IPv6 link-local 地址完全一致。S6
26. mdns_send_outpacket():DNS Header、pbuf 与 udp_sendto_if() 完成 Response 输出闭环
mdns_create_outpacket() 返回后,继续阅读 mdns_send_outpacket():S1
1 | err_t |
1 | flowchart TD |
这条链完成了“真实 PCAP 字段 ↔ DNS Question/RR ↔ parser/reply bitmap/packet builder ↔ multicast Response”的四线对应。
27. DNS-SD browse:当前 PCAP 已验证 Announcement 记录,但还没有捕获 _http._tcp 主动查询
本次 48 帧 PCAP 已经通过 Frame 20/22 证明 lwIP 发布了 _http._tcp.local → myweb._http._tcp.local → lwip.local:80 以及 TXT path=/。S6
但这份 capture 没有出现 Host 主动发出的 _http._tcp.local PTR browse Query。其中能看到 _ipp._tcp.local、_ipps._tcp.local 等 Avahi background browse,这是 Host 上其他服务发现行为,不应误写成当前 myweb 实验结果。S6
要继续补齐“外部 querier 主动 browse _http._tcp”这组证据,在 example 和 tcpdump 都保持运行时执行:
1 | avahi-browse \ |
预期目标是让 PCAP 多出一组 _http._tcp.local PTR Query,再观察 responder 返回 PTR/SRV/TXT/A/AAAA。这里只写成下一次可执行操作;在真正得到第二份 PCAP 前,不把预期结果冒充为本次抓包事实。
28. 抓包里为什么还有很多 wdfk-ubuntu24.local、_ipp._tcp、_smb._tcp
当前 capture filter 是整个 lwip0 上的 IPv4 udp/5353 or igmp,因此 Avahi daemon 自己的 hostname/service traffic 也会被抓进来。Frame 11~18、24~45 等可以看到 Host 自己的 wdfk-ubuntu24.local、打印服务 browse、SMB/device-info announcement。S6
分析 Stage 24 时最稳妥的过滤维度不是“所有 5353 packet 都属于 lwIP”,而是同时看:
1 | source IP = 198.18.0.200 |
Wireshark 可以先使用:
1 | udp.port == 5353 && ip.addr == 198.18.0.200 |
这能把 Host 自己的 Avahi 背景流量和 lwIP responder 主线分开。
29. Announcement、普通 Query Reply 与 lwIP Search API 是三种不同触发源
本次实验实际验证了两种触发源:Frame 20/22 是 responder 自己主动发出的 Announcement;Frame 46/47 是 Avahi Query 触发的 responder Reply。S6
当前 lwIP 在 LWIP_MDNS_SEARCH=1 时还可以主动充当 service-search client。mdns_search_service() 分配 mdns_requests[] slot、默认设置 qtype=PTR,再调用 mdns_send_request();远端 Response 回到同一个 mdns_recv() 后走 QR=1 分支进入 mdns_handle_response(),匹配 request 后调用应用 result_fn()。S1
继续阅读 mdns_search_service()。下面是按 IPv4 search 主线裁剪的执行路径阅读版:省略参数校验、only_ptr 特例和 IPv6 send 分支,只保留 request slot 填充与 IPv4 multicast request 提交。S1
1 | req = &mdns_requests[slot]; |
本次 Host 实验使用 Avahi 作为外部 querier,没有调用 lwIP 自己的 search API,因此这条只作为 implementation boundary,不与 Frame 46/47 混在一起。S1S6
30. mDNS、DNS-SD 与传统 DNS / IGMP 的边界
源码和 PCAP 都闭环后,再统一收束几个容易混淆的对象:
1 | IGMP |
mDNS IPv4 destination 固定为 224.0.0.251,对应 Ethernet multicast MAC 01:00:5e:00:00:fb;本次 Frame 1、2、20、22、47 都能直接看到这个 MAC/IP 目的组合。S3S6
31. 当前 resource limits 与实验边界
mdns_opts.h 默认:S1
1 |
这说明 service registry、pending/truncated packet 和 search request 都是真实 runtime resource;“支持 mDNS/DNS-SD”并不等于可以无限注册 service 或并行 request。
当前实验还存在三个明确边界:
- capture filter 只保存 IPv4 mDNS/IGMP,但 responder binary 是双栈,Announcement/Response 里仍可能携带 AAAA RR;
myweb._http._tcp.local:80是 example 发布的 service record,本轮没有验证 HTTPD 真正在 80 端口监听;- 当前 PCAP 验证了 Announcement 和 Host A Query/Response,但没有验证
_http._tcpbrowse Query,也没有验证 Known Answer suppression。
这些未验证项保留为未验证状态,不由源码“应该如此”替代真实运行证据。S1S6
32. Stage 24 的最终心智模型
1 | flowchart TD |
这一阶段真正建立的不是“记住 UDP 5353”,而是把四种证据连成一条链:
1 | example 配置 |
下一篇进入 AutoIP:当 DHCPv4 不可用时,lwIP 如何选择 169.254/16 candidate,并通过 ACD 的 Probe、Announce、Conflict 与 Defense 决定这个地址能否安全投入使用。
资料来源
[S1] lwIP mDNS responder/search 源码
- 类型:目标版本上游源码
- 版本:commit
d08f4773edd0182b7910fc8f046eed82ffcd67c9 - 定位:
src/apps/mdns/mdns.c:mdns_resp_init()、mdns_resp_add_netif()、mdns_probe_and_announce()、mdns_recv()、mdns_handle_question()、mdns_parse_pkt_questions()、mdns_parse_pkt_known_answers()、mdns_resp_add_service()、mdns_search_service();src/apps/mdns/mdns_out.c:mdns_create_outpacket()、mdns_send_outpacket()、mdns_send_request() - URL/文档:lwIP mDNS source
- 使用位置:“UDP 5353 transport”“Probe/Announce”“真实 Query parser”“Known Answer”“response policy”“RR 构造”“search boundary”
- 支撑内容:证明 responder/search 的真实调用链、状态对象、reply bitmap 与 output machinery
[S2] lwIP mDNS example 与 example_app 配置
- 类型:目标版本上游 example
- 版本:commit
d08f4773edd0182b7910fc8f046eed82ffcd67c9 - 定位:
contrib/examples/mdns/mdns_example.c;contrib/examples/example_app/test.c;lwipopts.h;lwipcfg.h.example - URL/文档:lwIP contrib examples
- 使用位置:“真实 application entry”“myweb._http._tcp”“TXT path=/”“LWIP_MDNS_APP”
- 支撑内容:提供真实入口和 service/TXT 示例,并区分 responder Core 编译能力与 app runtime enable
[S3] RFC 6762:Multicast DNS
- 类型:IETF 标准规范
- 版本:RFC 6762,2013
- URL/文档:RFC 6762
- 使用位置:“mDNS 前置阅读”“UDP 5353”“224.0.0.251”“Probe/Announce”“Known Answer”“multicast/unicast response”
- 支撑内容:提供 mDNS 的规范语义,并用于核对正文中的 UDP 5353、Probe/Announcement、Known Answer、冲突与 response policy
[S4] RFC 6763:DNS-Based Service Discovery
- 类型:IETF 标准规范
- 版本:RFC 6763,2013
- URL/文档:RFC 6763
- 使用位置:“DNS-SD 前置阅读”“service instance naming”“PTR/SRV/TXT 与 serializer 映射”
- 支撑内容:提供 DNS-SD service instance、PTR/SRV/TXT/A/AAAA 关系的规范依据,并用于核对 lwIP service object/serializer 映射
[S5] lwIP Source Lab Host 构建与实验约定
- 类型:目标项目脚本/README
- 版本:本轮实验仓库状态
- 定位:
scripts/configure-debug.sh、scripts/build.sh、README 的 Host 实验网络与“configure 后追加 stage override”约定 - 使用位置:“configure/build”“lwipcfg.h override”“TAP 198.18.0.1/24 ↔ 198.18.0.200/24”
- 支撑内容:保证本文操作命令与当前仓库真实入口一致
[S6] Stage 24 真实 Host 实验与 PCAP
- 类型:用户实验日志 + 抓包
- 时间:2026-10-02
- 环境:Ubuntu 24.04 Host;
lwip0;Host198.18.0.1/24;lwIP198.18.0.200/24 - 定位:
captures/stage24-mdns-ipv4.pcap;example_app启动日志;avahi-resolve -4 -n lwip.local输出 - 抓包范围:48 帧,约 52.291553 s,capture 结束时 0 kernel drop
- 使用位置:“IGMP/Probe”“Announcement 8 RR”“Frame 46/47 Host A Query/Response”“Avahi background traffic”“实验边界”
- 支撑内容:证明本次实际运行中的 mDNS group membership、Probe-style Query、两次 Announcement、PTR/SRV/TXT/A/AAAA 记录,以及
lwip.local → 198.18.0.200的真实解析结果
[S7] Apple Bonjour Concepts
- 类型:协议作者/平台厂商公开高层资料
- 版本:Apple Developer Archive,访问日期 2026-10-03
- URL/文档:Bonjour Concepts;About Bonjour
- 使用位置:“mDNS/DNS-SD 前置阅读”“service discovery 与 local-link 名称解析心智模型”
- 支撑内容:提供 Bonjour、mDNS 与 DNS-SD 的高层心智模型,作为正文自洽解释之外的进一步阅读与交叉核对









