開發與維運

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信息,可以嘗試升級網卡驅動。

image.png

接下來檢查send path,也就是發送情況。在老版本Windows操作系統,我們可以看mopen上的reference,原理是tcpip.sys driver每在發送的請求的時候都會增加tcpip與miniport的mopen的reference,而在網卡會在報文發送完成後,調用tcpip.sys驅動的回調函數釋放reference。

image.png

在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的統計信息,進一步確認發送正常,接收異常。

image.png

進一步看數據結構, NetReceiveBuffer List裡面是空的,NetNofReceiveBuffers 也是0,應該是網卡驅動發現沒有可用的buffer,導致就不能繼續收包。這裡面netkvm驅動有很多做法,比如在buffer滿的情況下disbale網卡中斷,也就不會有收報的行為發生。

image.png

後續檢查ParaNdis_ProcessRxPath函數和 virtqueue_get_buf 函數,確認ring buffer滿,

image.png

根據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"

image.png

注: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組件,以及使用其他軟件來替代當前使用的應用程序。

Leave a Reply

Your email address will not be published. Required fields are marked *