快思学习空间

首页/ 学习笔记/ mDNS 服务发现

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,否则收不到任何结果
AndroidNsdManager系统内置
HarmonyOS@ohos.net.mdns也可用原生 socket 自行处理组播
LinuxAvahi(avahi-browse / avahi-publish)桌面发行版一般预装

调试时两个好用的命令:

# macOS:浏览局域网内所有 HTTP 服务
dns-sd -B _http._tcp

# 任何平台:直接向多播地址发查询
dig @224.0.0.251 -p 5353 example.local

名称冲突与缓存

mDNS 没有中心机构分配名字,所以必须自己解决重名问题。协议规定的流程是:

  1. Probing(探测):设备启动时先发查询问「这个名字有人用吗」
  2. Announcing(宣称):确认无人应答后,才正式发布自己的记录
  3. 冲突改名:如果探测期间收到应答,说明重名,通常加后缀 (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 解决的是「找得到」的问题, 完全不解决「信不信得过」的问题。这两件事必须在设计上分开处理。

← 返回学习笔记 下一篇:JWT 与 JWKS →