天天看点

Windows Networking 1: 明明数据包已经到达网卡,为什么我的服务器不收包?

作者:陈鸽

Windows Networking 1: 明明数据包已经到达网卡,为什么我的服务器不收包?

前后端收发包的问题,往往排查起来颇为费劲。本系列以网络问题排查为基础,总结排查过程和分析结果,一步一步完善对 NDIS (Network Driver Interface Specification) Framework,以及对 Qemu Virtio netkvm 驱动的分析和研究。

问题

Windows Server 通过FTP上传文件,在传输过程中触发网络异常问题导致传输失败。尽管服务器网络经过一段时间后自动恢复,但是每次上传都很容易重现问题。

基本信息收集

问题描述粗看起来挺明确的,但是我们还是要理清楚具体细节问题,例如,

1.网络异常触发的条件:其他客户端通过FTP上传文件。

2.网络异常的现象:

  1. )在这台机器上ping 127.0.0.1,显示正常。此处说明Windows的TCPIP协议栈工作正常,问题发生在更底层驱动设备上。如果此处异常,一般我们考虑通过netsh.exe来reset tcpip和winsock。
  2. )这是一台经典网络主机,在问题发生的时候,内网网卡工作正常。具体来说就是,其他同内网网段机器ping这台Windows服务器正常,通过内网网卡ping内网dns正常。
  3. )问题发生的时候,这台机器尝试ping外网网卡上配置的网关不通。此处第2和第3步说明网卡驱动本身应该工作正常,问题可能在网卡或者该外网网卡所对应的NDIS Miniport,包括对应的NDIS.sys维护的NDIS_MINIPORT_BLOCK结构和网卡驱动维护的_ADAPTER结构
  4. )在尝试ping网关的时候,运行ARP -a 显示网关MAC和IP映射关系是Incomplete。说明数据包的发送已经尝试触发ARP过程,但是无法收到响应。
  5. )基于第4步,检查网卡设备(devmgmt.msc)显示状态正常,但是网卡接口(Network Connection -> NIC Properties)显示发送计数有增加,但接收计数却没有增加。这也侧面说明ARP请求发送正常,但是没有收到响应。
  6. )最后,尝试禁用启用网卡,发现问题解决。

3.根据异常现象排查,我们初步定位ARP问题,但通过arp.exe -s 去添加静态ARP信息(arp.exe -s gw.ipv4.address ee:ff:ff:ff:ff:ff),发现还是无法ping通网关,说明arp并不是主要的原因。

定位问题

为了更准确的定位问题,我们在Windows主机上以及它的宿主机上同时抓包分析。可惜的是,这次的抓包是直接打印在屏幕上,没有保留具体抓包内容。下次如果遇到类似情况,截图补上。

口述一下抓包的分析结果,添加静态ARP之后,VIF口上的抓包我们可以看到ping的ICMP Echo Request报文由vif口发送出去,并收到ICMP Echo Reply报文。

由于之前的种种测试排查,我们认为问题发生在底层驱动,由于问题现场存在,直接从NC上抓取Windows的dump分析。

技巧分享

Windows操作系统自2008 R2开始就集成了抓包能力,功能实现在NDIS.sys上,与Windows的ETW机制协同工作,为我们排查带来便利。启用的方式很简单,运行命令,

netsh trace start capture=yes

复现问题后,运行命令,

netsh trace stop

抓到的日志文件会被写在当前用户的temp目录下。当然,在运行stop命令后,Windows会把日志文件的位置打印在cmd.exe窗口中。

使用这个命令抓到的文件可以使用Microsoft Network Monitor 3.4或者Microsoft Message Analyzer打开。Wireshark暂时无法识别。

另外,额外提一句,Windows的ETW是一个比较好的排查操作系统内部组件行为的工具,Windows提供了一些既有的Scenarios和Providers,可以使用netsh trace show scenarios和netsh trace show providers来查看。有机会另写一篇文章作为补充。对于网络来说,我这边简单组合了一下ndis,tcpip,afd,winsock相关的providers,适用一般情况下对系统网络行为做一些比较深入的研究,命令如下,

netsh trace start provider={2F07E2EE-15DB-40F1-90EF-9D7BA282188A} keywords=0xffffffffffffffff level=0xff provider={E53C6823-7BB8-44BB-90DC-3F86090D48A6} keywords=0xffffffffffffffff level=0xff provider={7D44233D-3055-4B9C-BA64-0D47CA40A232} keywords=0xffffffffffffffff level=0xff provider={50B3E73C-9370-461D-BB9F-26F32D68887D} keywords=0xffffffffffffffff level=0xff provider={43D1A55C-76D6-4F7E-995C-64C711E5CAFE} keywords=0xffffffffffffffff level=0xff maxSize=500MB fileMode=circular persistent=no overwrite=yes report=yes correlation=yes traceFile=c:\NetworkTrace.etl capture=yes packettruncatebytes=128 IPv4.Address=<ipv4.address.for.filtering>           

具体命令含义可以参考netsh trace capture help

Memory Dump分析

首先查看网卡Miniport状态,并无异常状态。一般如果网卡异常我们可能会看到例如Pending OID,或者Reset等General信息,可以尝试升级网卡驱动。

Windows Networking 1: 明明数据包已经到达网卡,为什么我的服务器不收包?

接下来检查send path,也就是发送情况。在老版本Windows操作系统,我们可以看mopen上的reference,原理是tcpip.sys driver每在发送的请求的时候都会增加tcpip与miniport的mopen的reference,而在网卡会在报文发送完成后,调用tcpip.sys驱动的回调函数释放reference。

Windows Networking 1: 明明数据包已经到达网卡,为什么我的服务器不收包?

在Windows Server 2008 R2之后的版本,mopen的reference不再起作用,发送的计数被记录在tcpip.sys的Provider_Rundown_Protection中,以满足在不同的CPU上处理发送请求的能力。Rundown的计数针对每个CPU,通过加减法计算有没有pending NBL未发送。

参考资料:

https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/wdm/nf-wdm-exinitializerundownprotection

通过其他状态计数来确认发送接收情况:通过code review,我们找到了Virtio netkvm的统计信息,进一步确认发送正常,接收异常。

Windows Networking 1: 明明数据包已经到达网卡,为什么我的服务器不收包?

进一步看数据结构, NetReceiveBuffer List里面是空的,NetNofReceiveBuffers 也是0,应该是网卡驱动发现没有可用的buffer,导致就不能继续收包。这里面netkvm驱动有很多做法,比如在buffer满的情况下disbale网卡中断,也就不会有收报的行为发生。

Windows Networking 1: 明明数据包已经到达网卡,为什么我的服务器不收包?

后续检查ParaNdis_ProcessRxPath函数和 virtqueue_get_buf 函数,确认ring buffer满,

Windows Networking 1: 明明数据包已经到达网卡,为什么我的服务器不收包?

根据Virtio netkvm的代码,NetReceiveBuffersWaiting这个LIST_ENTRY数据结构里面的buffer内容其实是Windows 操作系统的NDIS 框架驱动负责维护,并调用netkvm驱动注册好的 ReturnPacketHandler 也就是 netkvm!ParaNdis5_ReturnPacket 来释放buffer并放回NetReceiveBuffers中。

在这里,问题集中在为什么NDIS没有调用我们的回调函数。Windows对网卡相关的缓存回收主要依靠NET_BUFFER_LIST数据结构的Reference,也就是引用计数。如果buffer被使用,那么它的reference count就会+1,如果buffer操作完成,对应引用的驱动会调用Dereference来释放引用。只有当buffer的reference计数变为0,才会回调释放buffer函数。

问题的定位是通过枚举所有未释放的buffer,打印出网络包结构,例如,

!list "-t \_LIST\_ENTRY.Flink -e -x \"dt netkvm!IONetDescriptor @$extret; dt ndis!_NDIS_PACKET poi(@$extret+0x40) Private.; dt _MDL poi(poi(@$extret+0x40)+8); db poi(poi(poi(@$extret+0x40)+8)+0x18) L0x50\" 0xfffffadf`37fa26d0"           
Windows Networking 1: 明明数据包已经到达网卡,为什么我的服务器不收包?

注:0x0bda = 3034 是FTP Pasv模式的data port。

本案例是Serv-U FTP服务器不处理收包导致buffer被用满,而通过使用Windows build-in的IIS FTP解决。

总结

综合前面的分析,一般情况下的下一步计划类似如下,

  1. Windows 本身NDIS驱动没有正确处理buffer的释放,对此,建议是安装好最新的 ndis.sys 补丁。
  2. 其他三方驱动对buffer有不正确的引用,导致引用计数一直无法为0,得到释放。建议是卸载三方,保持一个干净的操作系统。
  3. 报文没有被处理。
  • Possible Cause 1,数据包收到后需要通过消息通知机制indicate给上层Application,这里indication慢或者死锁会导致问题。
  • Possible Cause 2, 应用程序有Critical Section,导致没有recv操作发生等等。这中间就涉及了tcpip.sys, afd.sys, winsock, 以及应用程序本身,任何一环出问题都有可能引起网络问题。

对此,一般的建议是升级驱动tcpip.sys, afd.sys, 和 winsock组件,以及使用其他软件来替代当前使用的应用程序。

继续阅读