小Cの已经记不起来的博客

深入理解 Linux 服务发现 的工作机制

前几天把家里一台吃灰的旧笔记本装了 Debian,打算当个小服务器用。装完系统配好 ssh,我在另一台机器上随手敲了个 ssh user@debian.local,居然直接就连上了。当时有点懵,因为我压根没配 DNS,/etc/hosts 里也没加过东西,这名字是哪来的?后来顺着查了一圈资料才发现,背后是 mDNS 和 DNS-SD 这两套协议在撑着,也就是大家常说的 Zeroconf(零配置网络)。这里把我搞明白的东西整理一下,顺便记录排查时踩过的几个坑。

.local 的名字是谁在解析

先说结论:不是 DNS 服务器,你局域网里可能根本就没有 DNS 服务器。这套机制叫 mDNS(Multicast DNS),思路特别简单粗暴——当你要解析 debian.local 的时候,机器直接往组播地址 224.0.0.251 的 5353 端口发一个 UDP 包(IPv6 下对应 ff02::fb),意思是“谁是 debian.local,吱一声”。网段里所有开着 mDNS 服务的机器都能收到这个包,名字对得上的那台就把自己的 IP 塞进应答包发出来,全程不需要任何中心节点。

Linux 上干这活的通常有两个角色,一个是老牌的 avahi-daemon,桌面发行版基本都自带,服务器版没准儿要自己 apt install 一下;另一个是 systemd-resolved,systemd 体系里也实现了一份。以 avahi 为例,它起来之后会默认拿机器的 hostname 注册一个 xxx.local 的名字,也可以在 /etc/avahi/avahi-daemon.conf 里用 host-name 改。

还有一个容易忽略的环节:名字解析不是内核自己干的,而是 glibc 按 /etc/nsswitch.conf 里 hosts 那一行的顺序一个个试。典型配置长这样:

hosts: files mdns4_minimal [NOTFOUND=return] dns

mdns4_minimal 只管 .local 结尾的域名,别的名字它直接说“不归我管”,继续往后交给 dns;而 .local 的名字查不到时,[NOTFOUND=return] 会拦住,不让它去问上游 DNS。所以哪天手贱把这行改坏了,症状就是 .local 全挂、其他域名正常,挺迷惑人的。

光有名字还不够:DNS-SD 服务发现

mDNS 只解决了“名字换成 IP”这半件事,“服务发现”是另外半件,由 DNS-SD 来做。它的想法也很聪明:反正都是 DNS 那套记录,干脆把服务也编成域名。比如 _ssh._tcp.local 代表网内所有 SSH 服务,_ipp._tcp.local 代表打印机(对,打印机能被自动发现靠的就是这个),_http._tcp.local 就是各种带网页的管理界面。

每个服务实例由三段拼起来:实例名 + 服务类型 + 域名,比如 debian.local._ssh._tcp.local。浏览的时候先查 PTR 记录拿到实例列表,再对每个实例查 SRV(地址和端口)和 TXT(附加参数)。想看看你局域网里现在都有些什么,直接跑:

avahi-browse -art

-a 是浏览所有服务类型,-r 是顺手把每个服务解析出来,-t 是刷完一批就退出。我家里的网络跑一下就能看到路由器的管理页面、NAS 的 SMB,没准儿还能刷出几台你都没印象的设备,全都是机器自己“广播”出来的,没有任何一台配过什么发现服务器。

自己发布一个服务试试

被这个机制打动之后,就琢磨着能不能把自己机器上的东西也广播出去,结果发现简单得过分:往 /etc/avahi/services/ 里丢一个 .service 文件就行,比如广播 SSH:

<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
  <name replace-wildcards="yes">%h 的 SSH</name>
  <service>
    <type>_ssh._tcp</type>
    <port>22</port>
  </service>
</service-group>

保存后 avahi 会自己检测到文件变化,不用重启,avahi-browse -art 里马上就能看到,%h 会被替换成主机名。如果你在 macOS 上玩过 Bonjour,会发现这俩是同一套东西,直接互通。

抓个包看看它到底在干嘛

光看文档总觉得隔一层,直接上 tcpdump:

sudo tcpdump -i eth0 -n udp port 5353

然后另一台机器上跑个 avahi-browse -art,就能看到查询包是发往 224.0.0.251.5353 的——目的 IP 是组播地址,也就是说这个“问题”是喊给全屋人听的,而不是问某台服务器。里面还能看到一条查 _services._dns-sd._udp.local 的记录,这是在问“你们都提供哪类服务”,算是服务目录的目录。另外应答包里带了一个 cache-flush 标志位,意思是“以我这个应答为准,旧缓存作废”——这就是为什么机器换了 IP 之后,大部分客户端过一会儿自己就改过来了,不用手动刷什么缓存。

我踩过的几个坑

最大的坑是防火墙。mDNS 走 UDP 5353,而且收发都是组播,ufw 默认策略一开直接就掐了,症状是 avahi-browse 死活刷不出东西,但 ping IP 一切正常。放行就好:

sudo ufw allow 5353/udp

第二个坑是路由器。很多路由器开了“AP 隔离”或者访客网络,客户端之间互相不可见,组播包直接被吞,这种属于神仙难救,只能去路由器里把隔离关掉。第三个是 Docker,容器默认的 bridge 网络收不到宿主机网段的组播,容器里搞服务发现基本是废的,最省事的办法就是 --network host 跑。还有一个是 avahi-daemon 和 systemd-resolved 同时开的情况,两个都想监听 5353,轻则日志里互相报错,重则解析时灵时不灵,二选一就好。

最后说个排查思路:遇到 .local 连不上,先 systemctl status avahi-daemon 确认服务活着,再 avahi-browse -art 看能不能发现别的机器,最后 tcpdump 看包是没发出去还是没回来。就这么三板斧,基本能定位问题出在本机、防火墙,还是路由器。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放