乐趣区

Directory-Not-Empty-删不掉的幽灵

无法删除的文件
最近部门 NAS 测试团队遇到一个非常诡异的问题,在删除一棵存在 SMB 共享文件夹的文件树时,删除完子目录的所有文件后,再删除这个子目录的时候居然系统报出“Directory Not Empty”的错误从而导致用例测试未通过,打开这个目录一看,确实有一个文件并未删除成功,再查看 I / O 工具的日志,报告所有的文件都已经成功删除。难道是 I / O 工具出了问题而没有正确报出错误,这个经过工具开发者的研究后确认后貌似工具没有任何问题,这个问题起初被开发团队踢皮球,死活不承认是 SMB2 服务器的问题。死活非得让抓取网络包来证实。

作为一个网络分析的伪专家,自己也厚着脸皮主动蹭入 NAS 测试团队强行出力,希望把这个悬案搞个谁落石出。

抓包重现
首先,设置好抓包参数,同时重现问题,这一步很顺利。由于测试参数没有任何更改,很顺利的重现了问题,并且将出问题对应时间点的网络包悉数抓到。

在 Windows2106 的客户端开启抓包工具 wireshark(其对应的命令行工具为 tshark), 并且设置好相关参数:

$ tshark -i ens1 -B 4096 -s 1024 -w client-traffic.pcap

参数的含义就是在接口 ens1 上抓取长度为 1024 字节的每一个帧,并把这些帧存在 client-traffic.pcap 的文件里

分析
在抓取到网络包以后,我们开始解包分析:

同样通过 tshark 命令解包分析, 并将揭开的内容分别重定向到摘要文件 client-traffic.summary 和详细展开格式的 client-traffic.detail 文件当中:

$ tshark -t ud -Y "ip.addr==<server-ip>" -r client-traffic.pcap >>client-traffic.summary

$ tshark -t ud -O smb2 -Y "ip.addr==<server-ip>" -r client-traffic.pcap >>client-traffic.detail

打开 client-traffic.summary 文件后的一步就是要找到对应未删除成功的文件 (文件名: VCg8iMkGWgll2VJoEFMUa0FKp1DJHEG2) 最后一次出现的帧,简单的通过文本搜索便可定位,找到对应的帧以后发现,这个操作是一个 Create 操作,根据协议 [MS-SMB2], 删除操作是通过一组三元操作 Create/SetInfo/Close 来实现的,称为 Delete-On-Close,由于我们正要寻找的是删除操作,借此可以大胆推测,这个 Create (Frame #210725) 请求正是三元删除重的第一步, 再往后的帧一个个寻找,果然找到了第二步 SetInfo (Frame #210727) 和第三步 Close (Frame #210757, 通过该帧展开后确认其 FileID 属性与前两步操作对象一致) 两个请求以及对应的服务器端的回复:
最后 Close 并未得到服务器回复

Delete-On-Close 操作中的前两步
Delete-On-Close 操作中的前两步
然而最后的一步的 close 请求 (Frame #210757) 并未得到服务器端的回复,那么问题显然出在了这里,继续往下分析,为什么服务器没有对最后一步 close 操作作出响应呢?

最后 Close 并未得到服务器回复

奇怪的是,这个 close 操作发出后在 0.1 秒内没有得到任何回复,于是出发了 TCP 层的超时重传(RTO)(Frame #210765),按道理说在同一个实验室的内部网络下,网络状况是十分好的,基本上不可能发生 RTO 的情况(即便连快速重传也是极为罕见的),除非服务器出现了宕机。此时我突然想到我们测试当中为了测试 SMB2 的 CA 功能(Continues Availability:高可靠性,容忍服务器重启或者故障转移), 经常会引入错误注入的测试用例,再次翻看测试日志,果然发现在创建和删除文件操作的同时有重启服务器节点的操作执行,查看时间戳和发生问题的时间点一直,现在基本可以明确的是,发生的问题和 CA 有关了。

删除文件同时服务器重启

现在来看看正常的 CA 流程,当客户端一个 SMB2 请求遇到服务器重启的情况下,网络会暂时断开,发出的请求在若干次超时重传以后会收到服务重启后发出的 TCP 重置请求 (RST)(Frame #210769), 客户端在收到此请求后便可得知网络发生了断连,为是后续操作得以延续,必须再次建立 TCP 的连接(通过三次握手 Frame #210770, #210771, #210772), 重新协商(Negotiate. Frame #210773) 建立会话 (Session Setup. Frame #210776) 和共享文件根目录的连接 (Tree Connect. Frame #210785), 最后,因为客户端知道在网络断连之前最后一次没有响应的操作即目标文件,此时客户端会通过 Create 操作(Frame #210787) 重新发起一个对目标文件的连接(在 client-traffic.detail 可看到展开后的 Create 操作的 RECONNECT 信息),从而接续上服务器从其前的步骤,实现 CA 的功能特性

Frame #210773 – #210786 重建会话

在 client-traffic.detail 中找到第 210787 帧的展开后详细信息,便可得知这是一个试图重连 (RECONNECT) 操作:

Frame #210787 展开详细信息

到目前为止,客户端的所有操作均是按协议规定来进行的,那么是不是这次重连之后文件就能接续服务器重启前的操作进而把目标文件删除成功呢?要知道答案的话我们必须要检查一下服务对于重连操作的回复响应,找到服务器回复的那个帧,展开:

Frame #210790

服务器回答说,你重连的文件没有找到(STATUS_OBJECT_NAME_NOT_FOUND),这时候客户端理所当然的认为在服务重启之前目标文件已经成功删除,只是因为重启导致网络断连导致回复并未成功发出。所以客户端继续往下进行剩余文件的删除知道遇到删除目录出错(Frame #210838) 到这里我们可以肯定的是客户端是严格按照协议来进行请求的,而服务器这是在重连的回复里(Frame #210790) 错误地告知文件已经删除成功从而误导了客户端。所以这个已经非常明显是一个 SMB2 服务器的 Bug。

Frame #210838

我们看看协议里对于这一个服务器回复 (Frame #210790) 的定义:

SMB2 协议定义

如果 Persistent 的文件句柄并未在全局句柄表里找到(假设已经被成功 close), 那么便返回 STATUS_OBJECT_NAME_NOT_FOUND 的回复

结尾
根据这些网络包,再与开发团队确实交流后,他们接受了这个 Bug 并进行了修复。

可以看到,借助于 wireshark, 很多复杂且奇怪的问题都可以探究其背后秘密谈话,从而获取任何的蛛丝马迹找到问题的根源所在。

SMB2 协议:

退出移动版