Linux主機USB RNDIS網路卡驅動實現不完整導致的一例問題

bigfish99發表於2021-05-22

某通訊模組裝置,通過USB提供RDNIS和ECM網路卡功能。在實際應用中發現,USB RNDIS網路卡模式下,當使用AT指令以不同的CID撥號的時候,在Windows主機上能正常撥號成功,但在Linux主機上卻會發生撥號失敗的情況。作為對比,在同樣的測試環境和測試方法下,USB ECM網路卡則沒有這種異常。

 

測試流程概括如下:

- 裝置側已經配置為USB RNDIS模式

- 主機側通過AT指令以CID1撥號成功

- 測試網路功能,主機和裝置側可以ping通

- 主機側通過AT指令斷開CID1撥號

- 主機側通過AT指令以CID2撥號失敗,主機和裝置側不能ping通

 

以上是問題背景。

 

USB ECM是USB-IF定義的CDC類規範下的一個子類規範,全稱Ethernet Networking Control Model;RNDIS是微軟為即插即用的乙太網裝置制定的一種規範。實現這兩種協議的USB裝置,通過USB線接入主機後,會在主機側和裝置側各生成一張網路卡。兩側的網路卡處在同一個網段,進行網路通訊,資料承載通路是USB。下圖是從微軟官網摘抄的RNDIS框架圖:

 

https://docs.microsoft.com/en-us/windows-hardware/drivers/network/overview-of-remote-ndis--rndis-

 

Linux主機USB RNDIS網路卡驅動實現不完整導致的一例問題

 

經調查,Linux主機上RNDIS撥號測試失敗,主要原因在於:當第一次撥號成功後,斷開撥號時,Linux主機上的USB網路卡IP地址並沒有消失,後續以不同CID撥號後,Linux也沒有發起DHCP請求包,DHCP過程失敗,IP地址未更新。此時,如果將USB拔插一下就可恢復正常。

通過AT指令通知裝置端斷開撥號時,裝置側會有down USB網路卡的動作,down USB網路卡的過程中,裝置側RNDIS會上報rndis disconnect訊息來通知主機側。主機側可以根據這個訊息做相應處理。

 

在Ubuntu主機和Windows主機上測試斷開撥號操作時,在裝置端抓取的kernel log片段如下。可以看到,無論是在Windows主機上還是Ubuntu主機上,裝置端確實在斷開撥號時上報了rndis disconnect訊息。

Ubuntu主機環境,裝置端log:
root@udx710-module:~# 
[  324.516525] c0 configfs-gadget gadget: rndis_close
[  324.521239] c0 rndis_set_param_medium: 0 0
[  324.525296] c0 rndis_signal_disconnect
[  324.529023] c0 rndis_indicate_status_msg: status 1073807372
​
Windows主機環境,裝置端log:
root@udx710-module:~#  
[  191.340507] c1 configfs-gadget gadget: rndis_close
[  191.345223] c1 rndis_set_param_medium: 0 0
[  191.349285] c1 rndis_signal_disconnect
[  191.353008] c1 rndis_indicate_status_msg: status 1073807372
[  191.364621] c1 configfs-gadget gadget: rndis reqa1.01 v0000 i0000 l4096

 

裝置端動作

裝置端關鍵程式碼如下:

rndis_close -> rndis_signal_disconnect -> rndis_indicate_status_msg

drivers/usb/gadget/function/rndis.c
​
int rndis_signal_disconnect(struct rndis_params *params)
{
  params->media_state = RNDIS_MEDIA_STATE_DISCONNECTED;
  return rndis_indicate_status_msg(params, RNDIS_STATUS_MEDIA_DISCONNECT);
}
​
/*
 * Device to Host Comunication
 */
static int rndis_indicate_status_msg(struct rndis_params *params, u32 status)
{
  rndis_indicate_status_msg_type *resp;
  rndis_resp_t *r;
​
  if (params->state == RNDIS_UNINITIALIZED)
    return -ENOTSUPP;
​
  r = rndis_add_response(params, sizeof(rndis_indicate_status_msg_type));
  if (!r)
    return -ENOMEM;
  resp = (rndis_indicate_status_msg_type *)r->buf;
​
  resp->MessageType = cpu_to_le32(RNDIS_MSG_INDICATE);
  resp->MessageLength = cpu_to_le32(20);
  resp->Status = cpu_to_le32(status);
  resp->StatusBufferLength = cpu_to_le32(0);
  resp->StatusBufferOffset = cpu_to_le32(0);
​
  params->resp_avail(params->v);
  return 0;
}
​
drivers/usb/gadget/function/f_rndis.c
​
static void rndis_response_available(void *_rndis)
{
  struct f_rndis      *rndis = _rndis;
  struct usb_request    *req = rndis->notify_req;
  struct usb_composite_dev  *cdev = rndis->port.func.config->cdev;
  __le32        *data = req->buf;
  int        status;
​
  if (atomic_inc_return(&rndis->notify_count) != 1)
    return;
​
  /* Send RNDIS RESPONSE_AVAILABLE notification; a
   * USB_CDC_NOTIFY_RESPONSE_AVAILABLE "should" work too
   *
   * This is the only notification defined by RNDIS.
   */
  data[0] = cpu_to_le32(1);
  data[1] = cpu_to_le32(0);
​
  status = usb_ep_queue(rndis->notify, req, GFP_ATOMIC);
  if (status) {
    atomic_dec(&rndis->notify_count);
    DBG(cdev, "notify/0 --> %d\n", status);
  }
}

  

rndis_indicate_status_msg 會做兩件事:

1)把RNDIS_MSG_INDICATE message放入response queue,message中帶了status(RNDIS_STATUS_MEDIA_DISCONNECT)。

2)呼叫 resp_avail 發 RESPONSE_AVAILABLE notification(最終呼叫到rndis_response_available @ f_rndis.c),通過interrupt IN端點發出去,這個資料中沒有包含rndis連線狀態資訊,只是單純的上報RESPONSE_AVAILABLE (0x00000001)。

 

主機端動作

1)按微軟的RNDIS規範,Host收到RESPONSE_AVAILABLE後,接下來需要往control端點發 GET_ENCAPSULATED_RESPONSE request 才能讀取裝置端的 response,得到rndis連線狀態。

Upon receiving the RESPONSE_AVAILABLE notification, the host reads the control message from the Control endpoint using a GET_ENCAPSULATED_RESPONSE transfer, defined in the following table.

BmRequestTypebRequestwValuewIndex  
0xA1 0x01 0x0000      
https://docs.microsoft.com/en-us/windows-hardware/drivers/network/control-channel-characteristics

 

2)再看rndis host驅動:

rndis_status @ rndis_host.c 函式沒有做實際動作。

drivers/net/usb/rndis_host.c
void rndis_status(struct usbnet *dev, struct urb *urb)
{
  netdev_dbg(dev->net, "rndis status urb, len %d stat %d\n",
       urb->actual_length, urb->status);
  // FIXME for keepalives, respond immediately (asynchronously)
  // if not an RNDIS status, do like cdc_status(dev,urb) does
}
​
static const struct driver_info  rndis_info = {
  .description =  "RNDIS device",
  .flags =  FLAG_ETHER | FLAG_POINTTOPOINT | FLAG_FRAMING_RN | FLAG_NO_SETINT,
  .bind =    rndis_bind,
  .unbind =  rndis_unbind,
  .status =  rndis_status,
  .rx_fixup =  rndis_rx_fixup,
  .tx_fixup =  rndis_tx_fixup,
};

  

也就是說,Linux主機RNDIS驅動沒有嚴格按照RNDIS協議流程去讀取RNDIS_STATUS_MEDIA_DISCONNECT訊息,導致它無法獲知裝置端RNDIS網路卡斷開的狀態,進而無法正確作出網路狀態改變的相關處理。

 

而且作者也在程式碼註釋中表明瞭態度:強烈建議不要使用RNDIS,而應使用CDC乙太網(ECM,NCM,EEM等)這類非專有(non-proprietary)的替代方案。USB CDC規範是USB-IF制定的,RNDIS是微軟制定的。

/*
 * RNDIS is NDIS remoted over USB.  It's a MSFT variant of CDC ACM ... of
 * course ACM was intended for modems, not Ethernet links!  USB's standard
 * for Ethernet links is "CDC Ethernet", which is significantly simpler.
 *
 * NOTE that Microsoft's "RNDIS 1.0" specification is incomplete.  Issues
 * include:
 *    - Power management in particular relies on information that's scattered
 *  through other documentation, and which is incomplete or incorrect even
 *  there.
 *    - There are various undocumented protocol requirements, such as the
 *  need to send unused garbage in control-OUT messages.
 *    - In some cases, MS-Windows will emit undocumented requests; this
 *  matters more to peripheral implementations than host ones.
 *
 * Moreover there's a no-open-specs variant of RNDIS called "ActiveSync".
 *
 * For these reasons and others, ** USE OF RNDIS IS STRONGLY DISCOURAGED ** in
 * favor of such non-proprietary alternatives as CDC Ethernet or the newer (and
 * currently rare) "Ethernet Emulation Model" (EEM).
 */

  

最後順便看看為什麼ECM沒有問題。

 

ECM裝置側,ecm_close -> ecm_notify -> ecm_do_notify -> 通過interrupt IN端點發出去,這個資料中直接帶了ecm連線狀態,無需Host專門再發另外的request讀取這個狀態。

static void ecm_do_notify(struct f_ecm *ecm)
{
  struct usb_request    *req = ecm->notify_req;
  struct usb_cdc_notification  *event;
...
  event = req->buf;
  switch (ecm->notify_state) {
 ...
  case ECM_NOTIFY_CONNECT:
    event->bNotificationType = USB_CDC_NOTIFY_NETWORK_CONNECTION;
    if (ecm->is_open)
      event->wValue = cpu_to_le16(1);
    else
      event->wValue = cpu_to_le16(0);
    event->wLength = 0;
    req->length = sizeof *event;
​
    DBG(cdev, "notify connect %s\n",
        ecm->is_open ? "true" : "false");
    ecm->notify_state = ECM_NOTIFY_SPEED;
    break;
...
}

  

ECM主機側,usbnet_cdc_status @ cdc_ether.c 函式中有處理ecm connection訊息並呼叫usbnet_link_change。

void usbnet_cdc_status(struct usbnet *dev, struct urb *urb)
{
  struct usb_cdc_notification  *event;
...
  event = urb->transfer_buffer;
  switch (event->bNotificationType) {
  case USB_CDC_NOTIFY_NETWORK_CONNECTION:
    netif_dbg(dev, timer, dev->net, "CDC: carrier %s\n",
        event->wValue ? "on" : "off");
    usbnet_link_change(dev, !!event->wValue, 0);
    break;
...
  }
}
​
static const struct driver_info  cdc_info = {
  .description =  "CDC Ethernet Device",
  .flags =  FLAG_ETHER | FLAG_POINTTOPOINT,
  .bind =    usbnet_cdc_bind,
  .unbind =  usbnet_cdc_unbind,
  .status =  usbnet_cdc_status,
  .set_rx_mode =  usbnet_cdc_update_filter,
  .manage_power =  usbnet_manage_power,
};

 

版權所有,轉載請註明出處。

文章會同步到“大魚嵌入式”,歡迎關注,一起交流。

相關文章