mDNS 服务发现:从原理到容器里的坑
局域网里没有中心 DNS 服务器,设备之间怎么互相找到对方? 这篇笔记整理 mDNS 与 DNS-SD 的机制,以及动手时最容易踩的容器网络问题。
传统 DNS 的一个隐含前提
DNS 工作得很好,但它有一个不容易注意到的前提:网络里存在一台(或一组)地址已知的服务器, 并且所有人都能访问到它。在家庭局域网里这个前提不成立 —— 你没法要求每台路由器都跑一个 DNS 服务, 也没法让刚连上 Wi-Fi 的手机提前知道「客厅那台机器叫什么名字」。
mDNS 的做法:把 DNS 报文扔进多播
mDNS(Multicast DNS,RFC 6762)的思路非常直接:既然没有中心服务器, 那就把 DNS 的查询和应答直接发到多播地址上,让局域网里的设备自己认领。
- 多播地址:IPv4 是
224.0.0.251,IPv6 是ff02::fb - 端口:UDP
5353 - 域名后缀:
.local
想解析 printer.local,客户端就把一条普通的 DNS 查询报文发到
224.0.0.251:5353。局域网里所有监听该地址的设备都会收到,
名字对得上的那台用多播回一个应答,其余设备忽略。
值得强调的是:报文格式和普通 DNS 完全一样。 mDNS 是「换了个传输方式的 DNS」,不是一套新协议 —— 理解这一点之后,用
dig直接调试它就变得很自然。
DNS-SD:从「知道名字」到「发现服务」
能解析主机名还不够。客户端通常压根不知道设备叫什么, 它想问的是「这个网络里有没有打印机」。DNS-SD(RFC 6763)在 mDNS 之上叠了一层命名约定:
_<service>._<proto>.local
常见的服务类型:
_ipp._tcp.local— 网络打印机_airplay._tcp.local— AirPlay 接收端_googlecast._tcp.local— Chromecast_http._tcp.local— 普通 HTTP 服务
一个服务实例在 DNS 里由几条记录共同描述:
| 记录 | 作用 |
|---|---|
PTR | 服务类型 → 实例名列表,客户端靠它枚举「有哪些实例」 |
SRV | 实例名 → 主机名 + 端口 |
TXT | 实例名 → 自定义键值对,留给应用自己用 |
A / AAAA | 主机名 → IP 地址 |
TXT 是最有意思的一条 —— 它是协议留给应用的扩展位,可以塞版本号、
设备标识、能力标志之类的元数据,让客户端在真正建立连接之前就能做初步筛选:
服务类型: _nasbox._tcp.local
实例名: NASBox-1a2b3c4d._nasbox._tcp.local
端口: 8443
TXT 记录:
id=550e8400-e29b-41d4-a716-446655440000
ver=1.0.3
fp=AB:CD:EF:...
name=客厅的文件服务器
各平台的客户端 API
| 平台 | 接口 | 备注 |
|---|---|---|
| iOS / macOS | NWBrowser(Network.framework) |
需在 Info.plist 声明 NSBonjourServices;iOS 14+ 还需 NSLocalNetworkUsageDescription,否则收不到任何结果 |
| Android | NsdManager | 系统内置 |
| HarmonyOS | @ohos.net.mdns | 也可用原生 socket 自行处理组播 |
| Linux | Avahi(avahi-browse / avahi-publish) | 桌面发行版一般预装 |
调试时两个好用的命令:
# macOS:浏览局域网内所有 HTTP 服务
dns-sd -B _http._tcp
# 任何平台:直接向多播地址发查询
dig @224.0.0.251 -p 5353 example.local
名称冲突与缓存
mDNS 没有中心机构分配名字,所以必须自己解决重名问题。协议规定的流程是:
- Probing(探测):设备启动时先发查询问「这个名字有人用吗」
- Announcing(宣称):确认无人应答后,才正式发布自己的记录
- 冲突改名:如果探测期间收到应答,说明重名,通常加后缀
(2)再试
缓存方面,mDNS 记录的 TTL 建议值是 120 秒。协议还设计了 known-answer suppression 之类的机制来减少多播风暴 —— 毕竟多播包会送到网段内每一台设备,浪费的是所有人的带宽。
容器里的坑
这是实际动手时最容易卡住的地方,而且症状很有迷惑性:一切配置看起来都对,就是发现不了。
原因在于 Docker 默认的 bridge 网络下,容器拥有自己独立的 network namespace 和虚拟网卡。
多播包发不到宿主机的物理网段,物理网段里的多播包也进不到容器。
结果就是容器内 avahi-browse 一片空白,或者容器发布的服务在外面完全看不见。
| 方案 | 做法 | 代价 |
|---|---|---|
| host 网络 | network_mode: host,容器直接使用宿主机网络栈 |
最简单直接;但端口不再隔离,容器端口直接占用宿主机端口 |
| 共享 avahi | 挂载宿主机的 /var/run/dbus 与 /etc/avahi,容器内跑 avahi-daemon |
保留端口隔离;配置较繁琐,还要处理两个 avahi 实例的协调 |
| 宿主机发布 | 由宿主机的 avahi 用静态服务文件发布记录,容器只提供 HTTP 服务 | 最稳定;但需改宿主机配置,容器无法自主增删服务记录 |
如果服务发现并非强需求,还有一个很务实的选择:干脆不用 mDNS, 改由客户端主动扫描网段,或经由其他信令通道拿到地址。 功能不受影响,只是发现过程会慢一点。
几个容易忽略的局限
- 一般不跨网段。路由器默认不转发多播, VLAN 之间、访客网络与主网络之间通常互相看不见。
- 依赖底层网络配合。部分 AP 开启了多播抑制或多播转单播, 会直接破坏 mDNS。
- 没有任何认证机制。任何人都能发布同名服务。 「发现到」不等于「可信」 —— 拿到地址之后, 仍然需要一套独立的身份校验(比如 TLS 证书固定)才能建立信任。
最后这条是我觉得最值得记下来的:mDNS 解决的是「找得到」的问题, 完全不解决「信不信得过」的问题。这两件事必须在设计上分开处理。