广播过不了 WireGuard:一次小米摄像头跨网发现 SMB 的抓包与修复

米家只显示“没有可用的网络存储”。真正的问题不是 SMB 不通,而是摄像头根本不知道该去连谁。


目录


问题:445 端口能通,米家却看不到 NAS

网络结构很常见:

摄像头局域网                       WireGuard                    NAS 网段
192.168.1.0/24  <------------------------------------->  10.99.0.0/24

Xiaomi Camera                                                 Samba
192.168.1.50                                              10.99.0.10

两边三层路由已经打通。局域网里的普通主机可以直接访问
10.99.0.10:445,Samba 用户和共享目录也都正常,但米家添加 NAS 时只显示
“没有可用的”。

这类故障很容易被误判成:

  • WireGuard 没放行 TCP 445;
  • Samba 用户名或密码错误;
  • 摄像头只支持 SMB1;
  • 远端 NAS 没有开启“网络发现”。

但这些都解释不了一个事实:此时摄像头甚至还没有尝试连接 TCP 445。
米家界面不允许手动填写服务器 IP,它必须先通过局域网发现拿到服务器列表。

问题因此从“SMB 为什么连不上”变成了“摄像头用什么协议发现 SMB”。

抓包:摄像头到底发了什么

在摄像头所在网桥抓 UDP 137、138 和 TCP 445:

tcpdump -ni br-lan \
  'host 192.168.1.50 and (udp port 137 or udp port 138 or tcp port 445)'

打开米家的 NAS 添加页面后,摄像头每隔约 2 秒发出一次 UDP 137 广播。包长
50 字节,核心字段是:

Transaction ID: 随机变化
Flags:          0x0000 (query)
Questions:      1
Name:           *<00>
Type:           NB (0x0020)
Class:          IN (0x0001)

线上的名字并不是明文 *,而是 NetBIOS first-level encoding:

20 43 4b 41 41 41 41 ... 41 00
   C  K  A  A  A  A ... A

* 的字节值是 0x2a,高低半字节分别编码成 CK;后面的空字节都编码
AA。所以整个查询实际是在问:

局域网里有没有提供 NetBIOS 文件服务的机器?

广播目的地址是 192.168.1.255:137。WireGuard 是三层隧道,不会自动转发
这个二层广播,因此远端 NAS 永远收不到查询。

第一个坑:remote announce 不是它要的协议

Samba 有一个看起来很对症的配置:

[global]
    remote announce = 192.168.1.50/WORKGROUP

但抓包显示,nmbdremote announce 发出的是 UDP 138 浏览服务数据报。
测试的摄像头没有监听这条路径,甚至会对它返回 ICMP port unreachable。

摄像头真正依赖的是 UDP 137 上主动发起的 NBNS 查询与应答,而不是被动接收
浏览列表。

这也是排查广播协议时很容易踩的坑:“都叫 NetBIOS 发现”不代表它们是同一
条报文路径。

第二个坑:普通 UDP relay 能显示列表,却无法登录

第一版代理很直接:

  1. 在本地网桥监听摄像头的 UDP 137 广播;
  2. 把原包单播给远端 NAS;
  3. 收到 NAS 的 NBNS 应答;
  4. 用普通 UDP socket 把应答发回摄像头。

为了让列表完整出现,代理还转发了后续的 NBSTAT(0x0021)查询。结果米家
真的显示出了 NAS 名称。

但输入正确的用户名密码后,仍然提示“添加失败”。

继续抓 TCP 445,才发现摄像头连接的是:

192.168.1.50 -> 192.168.1.1:445

192.168.1.1 是运行代理的路由器,而真正的 NAS 是 10.99.0.10

NBNS 应答正文里明明已经写了 10.99.0.10,为什么摄像头仍然连路由器?

实测结论是:这版小米固件把 NBNS 回包的 IPv4 源地址当成服务器地址,而
不是使用 NB answer RDATA 中返回的地址。

普通 UDP relay 的回包源地址必然是路由器,所以:

列表阶段:路由器继续代答 NBSTAT,NAS 名称能显示
登录阶段:摄像头直接连接路由器 TCP 445,必然失败

这解释了一个非常迷惑的中间状态:能看到服务器,不代表发现流程返回的服务器
地址就是对的。

最终方案:只代理发现,并透明伪装回包源地址

最终程序仍然把查询单播给远端 NAS,但不再使用普通 UDP socket返回。它构造
完整的 IPv4 + UDP 报文:

Source IP:    10.99.0.10
Source Port:  137
Destination:  摄像头原始 IP 与临时端口
Payload:      经过校验的原始 NBNS 应答

报文通过 raw socket 从摄像头侧网桥发出,并重新计算 IP、UDP checksum。

完整时序变成:

sequenceDiagram
  participant C as 小米摄像头 192.168.1.50
  participant P as OpenWrt / NBNS Proxy
  participant N as 远端 NAS 10.99.0.10

  C->>P: 广播 NB *<00> 查询 (UDP 137)
  P->>N: 单播转发原查询
  N-->>P: NB 应答,RDATA = 10.99.0.10
  P-->>C: 透明回包,源 IP = 10.99.0.10
  C->>N: 直接发送 NBSTAT 查询
  N-->>C: 返回服务器名与 <20> 文件服务记录
  C->>N: 直接连接 TCP 445
  C->>N: SMB 认证、列目录与录像写入
  N-->>C: SMB 响应

这里没有代理 SMB,也没有给两个网段做二层桥接。代理只在发现阶段“补”一个
本该由远端服务器发出的 NBNS 回包;摄像头拿到地址后,NBSTAT 和 SMB 都直接
走已有的 WireGuard 三层路由。

代码如何收紧安全边界

Raw socket 和源地址伪装不能写成一个无条件广播中继。公开版本做了几层限制:

检查 目的
固定 --client 只接受指定摄像头的查询
固定 --upstream 只向指定 NBNS/SMB 服务器查询
严格匹配 50 字节查询 只处理实测的 *<00> NB/NBSTAT 包
连接式上游 UDP socket 内核只接收指定 IP、端口的返回
校验 Transaction ID 防止错配或旧应答被转发
校验响应位和 RCODE 只接受成功的 NBNS response
校验 NB answer 地址 返回地址必须等于配置的上游
校验 NBSTAT <20> 唯一名 必须包含文件服务器服务记录
空闲超时 添加完成后自动退出,不常驻扩大攻击面

程序不会记录 SMB 账号密码,因为这些数据根本不会经过它。运行时仍需要
CAP_NET_RAW 和低端口绑定权限,因此最稳妥的方式是在可信 OpenWrt 路由器上
按需启动,添加完成后停止。

相比“把整个广播域打通”,这个方案的边界要小得多:

一个客户端 IP + 一个上游 IP + 一种精确报文 + 一个短时间窗口

部署与使用

项目提供 Linux x86-64 静态二进制,可直接复制到 x86-64 OpenWrt:

gh release download v1.0.0 \
  --repo tinymins/xiaomi-nbns-proxy \
  --pattern 'xiaomi-nbns-proxy-linux-amd64*'

sha256sum -c xiaomi-nbns-proxy-linux-amd64.sha256
scp xiaomi-nbns-proxy-linux-amd64 root@192.168.1.1:/tmp/

先确保:

  1. 摄像头配置了 DHCP 静态租约;
  2. 路由器能访问远端 NAS 的 UDP 137;
  3. 摄像头能通过路由访问远端 NAS 的 TCP 445;
  4. 远端 Samba 的 nmbd 正常响应;
  5. 没有为了排错盲目把 SMB 协议降级到 SMB1。

在路由器运行:

chmod +x /tmp/xiaomi-nbns-proxy-linux-amd64

/tmp/xiaomi-nbns-proxy-linux-amd64 \
  --interface br-lan \
  --client 192.168.1.50 \
  --upstream 10.99.0.10 \
  --idle-timeout 900

看到类似日志:

listening interface=br-lan client=192.168.1.50 upstream=10.99.0.10
query client=192.168.1.50:49065 txid=0x313d type=0x0020
response client=192.168.1.50:49065 txid=0x313d type=0x0020 source=10.99.0.10 length=62

此时进入米家 NAS 设置,选择出现的服务器,输入 SMB 账号密码,再选择共享
目录。添加完成后可以 Ctrl+C 停止代理,也可以等空闲超时自动退出。

已保存的 SMB 配置通常会直接重连;只有删除 NAS 配置、恢复出厂设置或再次
执行服务器发现时,才需要重新运行代理。

其他 CPU 架构可以直接在对应 Linux/OpenWrt 环境编译:

make
make check

怎么判断每一段是否真的成功

不要只看米家界面的一个“成功/失败”提示。抓包可以把问题切成四段:

tcpdump -ni any \
  'host 192.168.1.50 and (udp port 137 or tcp port 445)'
现象 应该检查什么
代理没有 query 日志 接口是否选对、摄像头 IP 是否固定、米家是否触发发现
有 query,但没有 response WireGuard 路由、UDP 137、防火墙、远端 nmbd
服务器名出现,但登录失败 TCP 445 的目标 IP 是否真的是远端 NAS
已连远端 445,但认证失败 Samba 用户、密码、协议和 ACL
认证成功,但没有共享目录 用户是否至少拥有一个可浏览共享
添加成功,但无法录像 共享目录写权限、空间和文件锁

最终验证不应停在 TCP 三次握手。我在服务端继续检查了三个状态:

SMB dialect:  SMB 3.1.1
Tree connect: 目标共享已挂载
Open files:   摄像头目录中的 MP4 正在读写

只有这三项都成立,才能说“跨网 SMB”真的完成了,而不是列表恰好能显示。

开源与适用范围

源码、双语 README、构建方法和静态二进制已经发布:

当前实现只支持 IPv4,每个进程只处理一组摄像头和服务器,并严格匹配这次抓到
的小米 NBNS 查询格式。不同型号或固件可能改变发现协议,因此它不是一个通用
的“广播穿透器”。

这次排障最有价值的结论,不是“广播过不了 WireGuard”——这件事从一开始就
知道。真正关键的是:

当列表出现但登录仍然失败时,不要停在应用层提示。继续追踪下一跳,看看设备
最终把谁当成了服务器。

一个 NBNS 回包的源地址,决定了后面整条 SMB 连接链路。