广播过不了 WireGuard:一次小米摄像头跨网发现 SMB 的抓包与修复
米家只显示“没有可用的网络存储”。真正的问题不是 SMB 不通,而是摄像头根本不知道该去连谁。
目录
- 问题:445 端口能通,米家却看不到 NAS
- 抓包:摄像头到底发了什么
- 第一个坑:remote announce 不是它要的协议
- 第二个坑:普通 UDP relay 能显示列表,却无法登录
- 最终方案:只代理发现,并透明伪装回包源地址
- 代码如何收紧安全边界
- 部署与使用
- 怎么判断每一段是否真的成功
- 开源与适用范围
问题: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,高低半字节分别编码成 C、K;后面的空字节都编码
成 AA。所以整个查询实际是在问:
局域网里有没有提供 NetBIOS 文件服务的机器?
广播目的地址是 192.168.1.255:137。WireGuard 是三层隧道,不会自动转发
这个二层广播,因此远端 NAS 永远收不到查询。
第一个坑:remote announce 不是它要的协议
Samba 有一个看起来很对症的配置:
[global]
remote announce = 192.168.1.50/WORKGROUP
但抓包显示,nmbd 的 remote announce 发出的是 UDP 138 浏览服务数据报。
测试的摄像头没有监听这条路径,甚至会对它返回 ICMP port unreachable。
摄像头真正依赖的是 UDP 137 上主动发起的 NBNS 查询与应答,而不是被动接收
浏览列表。
这也是排查广播协议时很容易踩的坑:“都叫 NetBIOS 发现”不代表它们是同一
条报文路径。
第二个坑:普通 UDP relay 能显示列表,却无法登录
第一版代理很直接:
- 在本地网桥监听摄像头的 UDP 137 广播;
- 把原包单播给远端 NAS;
- 收到 NAS 的 NBNS 应答;
- 用普通 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/
先确保:
- 摄像头配置了 DHCP 静态租约;
- 路由器能访问远端 NAS 的 UDP 137;
- 摄像头能通过路由访问远端 NAS 的 TCP 445;
- 远端 Samba 的
nmbd正常响应; - 没有为了排错盲目把 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、构建方法和静态二进制已经发布:
- GitHub:tinymins/xiaomi-nbns-proxy
- Release:v1.0.0
- 许可证:MIT
当前实现只支持 IPv4,每个进程只处理一组摄像头和服务器,并严格匹配这次抓到
的小米 NBNS 查询格式。不同型号或固件可能改变发现协议,因此它不是一个通用
的“广播穿透器”。
这次排障最有价值的结论,不是“广播过不了 WireGuard”——这件事从一开始就
知道。真正关键的是:
当列表出现但登录仍然失败时,不要停在应用层提示。继续追踪下一跳,看看设备
最终把谁当成了服务器。
一个 NBNS 回包的源地址,决定了后面整条 SMB 连接链路。
版权属于:一名宅。
本文链接:https://zhaiyiming.com/archives/79.html
转载时须注明出处及本声明