背景
最近在实现一个基于 USB/IP 的 U 盘重定向功能。整体架构大致如下:
- 本地 Windows 客户端通过
libusb + UsbDk访问真实 USB 设备; - 客户端接收远端 USB/IP 请求;
- 本地通过
libusb_control_transfer、libusb_bulk_transfer等接口完成真实 USB 通信; - 服务端 Linux 通过
vhci_hcd创建虚拟 USB 设备。
测试过程中,某款 Kingston DataTraveler 3.0 U 盘在多次重定向、断网重连后,偶现服务端无法正常枚举设备,最终远端无法出现可用盘符或块设备。
现象
服务端 dmesg 中可以看到类似日志:
unable to get BOS descriptor or descriptor too short
unable to read config index 0 descriptor/all
can't read configurations, error -110
unable to enumerate USB device
其中 error -110 对应 Linux 的 ETIMEDOUT,说明服务端 USB 栈在枚举阶段等待描述符响应超时。
客户端日志中也能看到 libusb_control_transfer 返回超时:
libusb_control_transfer err: -7
descType=2
-7 对应:
LIBUSB_ERROR_TIMEOUT
最初从日志看,失败点主要集中在:
- BOS descriptor,
descType = 15 - Configuration descriptor,
descType = 2
因此一开始怀疑是 BOS 或 config descriptor 请求本身存在问题。
排查过程
为了避免只依赖完整产品流程,我们单独写了一个测试程序,对同一个 U 盘反复执行不同类型的 descriptor 请求。
测试项包括:
GET_DESCRIPTOR DEVICE, wLength = 8
GET_DESCRIPTOR DEVICE, wLength = 18
GET_DESCRIPTOR BOS
GET_DESCRIPTOR CONFIG
libusb_get_device_descriptor 缓存路径
libusb_get_config_descriptor 缓存路径
经过多轮测试,发现一个关键现象:
- 单独反复请求 BOS descriptor,基本稳定;
- 单独反复请求 config descriptor,基本稳定;
- 使用
libusb_get_device_descriptor()缓存路径,基本稳定; - 但反复真实下发
GET_DESCRIPTOR DEVICE请求时,较容易出现 timeout; wLength = 8和wLength = 18都可能触发。
也就是说,表面上服务端失败点可能落在 BOS/config descriptor,但真正更可疑的是前面反复执行的 device descriptor control 请求。
为什么会有 8 字节 Device Descriptor 请求
USB 枚举过程中,主机经常会先读取 device descriptor 的前 8 字节,用于获取:
bMaxPacketSize0
也就是端点 0 的最大包大小。
完整 device descriptor 长度是 18 字节,但请求中的 wLength 不一定只会是 18。常见请求包括:
wValue = 0x0100, wLength = 8
wValue = 0x0100, wLength = 18
甚至也可能出现更大的 wLength,设备通过 short packet 返回实际的 18 字节。
代码中的问题
原来的代码已经对完整的 18 字节 device descriptor 做了特殊处理:
if (GET_DESCRIPTOR DEVICE && wLength == LIBUSB_DT_DEVICE_SIZE)
{
libusb_get_device_descriptor(...);
}
也就是说,当远端请求 18 字节 device descriptor 时,本地不会真实下发 control transfer,而是直接使用 libusb 缓存的 device descriptor。
但是对于 8 字节请求,旧逻辑没有覆盖,仍然会走:
libusb_control_transfer(...)
实际日志中也验证了这一点:
route=control
wValue=0x100
wLength=8
descType=1
这意味着每次重定向或断网重连时,都会真实向设备发送一次 GET_DESCRIPTOR DEVICE, wLength=8。
修复思路
修复方向是:所有标准 device descriptor 请求都走缓存路径,而不是只处理 18 字节请求。
判断条件从:
wLength == LIBUSB_DT_DEVICE_SIZE
改为:
wLength > 0
并返回:
min(wLength, LIBUSB_DT_DEVICE_SIZE)
示例逻辑:
if (requestType == LIBUSB_ENDPOINT_IN &&
bRequest == LIBUSB_REQUEST_GET_DESCRIPTOR &&
wValue == (LIBUSB_DT_DEVICE << 8) &&
wIndex == 0 &&
wLength > 0)
{
struct libusb_device_descriptor desc = {0};
status = libusb_get_device_descriptor(dev, &desc);
if (status == LIBUSB_SUCCESS)
{
desc.bcdUSB = libusb_cpu_to_le16(desc.bcdUSB);
desc.idVendor = libusb_cpu_to_le16(desc.idVendor);
desc.idProduct = libusb_cpu_to_le16(desc.idProduct);
desc.bcdDevice = libusb_cpu_to_le16(desc.bcdDevice);
uint16_t len = std::min<uint16_t>(wLength, LIBUSB_DT_DEVICE_SIZE);
memcpy(data, &desc, len);
status = len;
}
}
这里需要注意,libusb_get_device_descriptor() 返回的是 host-endian 结构体,而 USB descriptor 在线路上是 little-endian。因此多字节字段最好通过 libusb_cpu_to_le16() 转换后再返回。
结论
这次问题的核心不是简单的 BOS/config descriptor 请求失败,而是:
在
libusb + UsbDk + Kingston DataTraveler 3.0的组合场景下,反复真实下发GET_DESCRIPTOR DEVICE请求,尤其是 8 字节请求,可能导致设备或 EP0 control 通道进入不稳定状态,后续 BOS/config 请求表现为 timeout。
修复后,将所有 device descriptor 请求统一改为使用 libusb_get_device_descriptor() 的缓存结果返回,避免重复真实访问设备 EP0,问题得到规避。
经验总结
- descriptor timeout 的失败点不一定是真正根因。
- USB 枚举中的 8 字节 device descriptor 请求非常常见,不能只处理 18 字节版本。
libusb_get_device_descriptor()是缓存路径,不会再次向设备发送 control 请求。- 在 USB/IP 重定向场景中,尽量减少对 EP0 的重复真实请求。
- 对特殊 U 盘兼容性问题,最好用独立测试程序拆分验证,而不是只看完整产品日志。