vdi-cloud/docs/usb-device-poc-20261103.md

6.4 KiB
Raw Blame History

U 盘真实 USB 设备透传(2026-11-03)

用户明确选择 Windows 必须识别真实 USB,并配合插盘、换口和 Windows 安全弹出后实际拔插。 当前为一个盒子、一个登记设备、一个 VM 的 POC,不是多用户生产功能。

已验证的现场结果

项目 结果
U 盘 Kingston DataTraveler 3.0,0951:1666,USB 描述符序列号 408D5C1653D4E44179789251
盒子 192.168.1.188,物理路径 3-1,实际 USB2 / 480 Mb/s 链路
Windows PVE starhe / VM100 / 192.168.1.70,USB Mass Storage 状态 OK
磁盘 Kingston DataTraveler 3.0 USB Device,InterfaceType=USB,USBSTOR 实例含原序列号
卷 KINGSTON(F:),FAT32,Removable,约 57.7 GiB,Healthy
文件读写 1 MiB 随机文件:源文件、USB 文件、复制回 VM 的文件 SHA-256 一致
实物拔插 用户先在 Windows 安全弹出,17:34:43 断开、17:34:53 重新导出,Windows F: 自动恢复
拔插后数据 再次读取测试文件,SHA-256 不变
独占 盒子 usbfs 持有接口;透传时 Linux 无该 U 盘的本地块设备挂载
原有会话 Moonlight PID46934 / 2K45 保留,显示守护 active,GTX965M 错误码 0

只新增 F:\VDI_USB_TEST_20261103-173342\from-windows.bin 和 readme.txt。 未格式化、改分区、覆盖或删除原文件。源和回读副本在 VM 的 C:\Windows\TEMP\VDI_USB_TEST_20261103-173342。

SHA-256:8244F1189EFA02118BF0DC4CB68FC8924FE562AC610B68DEEFBE09EC85454D51。 原始验收记录见 evidence/usb-device-20261103/。

自动检查:盒子 Rust 测试 5/5、管理员 QMP 测试 3/3,通过;Python 编译检查和 git diff --check 通过。现场还实测了“本地挂载时拒绝导出”,随后正常卸载才导出。

实现

盒子物理 USB -> Rust vdi-usb-monitor -> usbredirect/libusb(回环40001)
  -> PVE 发起的 SSH 加密隧道 -> QEMU usb-redir(现有 ehci-2.0)
  -> Windows 自带 USB Mass Storage / USBSTOR -> F:

usbredir 独立于 SPICE;显示通道仍为 Moonlight/Sunshine。 没有安装 Windows 第三方驱动、开测试签名、替换控制器、重启机器或改 GPU 配置。 盒子不调用 PVE API,也不保存 PVE 管理凭据。

Rust 使用标准库,无第三方 crate;等待设备每 2 秒扫描,活动连接每秒检查身份与地址。 按 VID/PID/serial 匹配唯一设备,并使用当前 bus-address 打开精确实例。 设备消失、匹配多实例、含非存储接口、已本地挂载、存在块设备 holder 或被用作 swap 时拒绝。 udev 规则仅对登记设备设置 UDISKS_IGNORE=1,防止本地自动挂载与 Windows 并发写盘。

SSH 使用独立密钥、已有可信主机密钥、源 IP 和 permitopen 限制,禁止该密钥执行 shell。 USB 监听与隧道两端均为回环地址,未暴露 LAN USB 服务端口。 Rust 为终端模块;Python 是管理员部署/QMP 工具;PowerShell 是 Windows 验收工具。

部署和管理

盒子先安装 rustc cargo usbredirect,在 /home/armbian/vdi-build/usb-monitor 运行 cargo test --offline、cargo build --release --offline。没有整体升级系统。 管理员机器需要 paramiko、可信 SSH 主机密钥,以及 PVE SSH 公钥或 VDI_PVE_SSH_PASSWORD 环境变量。部署器不保存密码。

python tools/deploy-usb-device-poc.py --pve 192.168.1.51 --box armbian@192.168.1.188 \
  --vm 100 --vendor 0951 --product 1666 --serial 408D5C1653D4E44179789251

确认设备身份、关闭本地文件操作并正常卸载对应分区,然后启动盒子服务: sudo systemctl start vdi-usb-monitor.service。本次分区为 /dev/sda1, 使用了 udisksctl unmount --block-device /dev/sda1;其他环境不能盲目照抄设备名。

在 PVE root 执行:

python3 /usr/local/libexec/vdi-usb-device-qmp.py attach --vm 100 --vendor 0951 --product 1666
python3 /usr/local/libexec/vdi-usb-device-qmp.py status --vm 100

QMP 只管理 vdi_usb_box_100 / vdi_usb_redir_100,不操作已有 tablet、GPU、网卡。 重复 attach 拒绝覆盖;device_add 失败回收仅本次新建 channel。 QEMU filter 内部分隔符为冒号,与 spice-client 的逗号不同。

位置 文件
盒子 /usr/local/libexec/vdi-usb-monitor
盒子白名单 /etc/vdi-usb-monitor/device.env
盒子挂载策略 /etc/udev/rules.d/99-vdi-usb-device-poc.rules
盒子服务 /etc/systemd/system/vdi-usb-monitor.service
PVE 密钥 /etc/vdi-usb-poc/vm100/
PVE 隧道 /etc/systemd/system/vdi-usb-tunnel-100.service
盒子原始备份 /home/armbian/vdi-terminal/backups/usb-device-20261103-172709/

两端服务已 active/enabled,未通过重启验证开机顺序。重新部署前须在 Windows 安全弹出, 停止盒子导出服务;部署器拒绝覆盖运行中的服务。

限制

  • 仅登记的这一只 U 盘;其他设备需管理员登记。没有多终端隔离、会话授权/切换或 Qt 状态提示。
  • 固定 IP、单端口、单 VM POC。QMP 设备仅存在于当前运行实例,VM/PVE 重启后须重新 attach; 没有修改原 VM args 或 GPU 启动设置。
  • 验证了当前 FAT32 U 盘、1 MiB 读写和一次实物热插拔;未测大文件吞吐、NTFS/exFAT、 写入中断网/掉电恢复或长期老化。
  • 最初接口报 USB 枚举 -71,换到 3-1 对应口才成功;并非软件修好了原口,也未验证 USB3 速度。
  • 使用后正常安全弹出;程序不会重试用户文件写入,不能保证强拔/断网时的数据完整性。

回退

  1. 关闭 U 盘文件,在 Windows 正常安全弹出。
  2. PVE:python3 /usr/local/libexec/vdi-usb-device-qmp.py detach --vm 100 --guest-ejected。
  3. 盒子:sudo systemctl disable --now vdi-usb-monitor.service;PVE: systemctl disable --now vdi-usb-tunnel-100.service。
  4. 仅移除/恢复本功能 udev 规则,udevadm control --reload-rules,再拔插让 Linux 本地重新识别。
  5. 如移除密钥,只删 authorized_keys 中 vdi-usb-poc-vm100 对应公钥行;保留其他公钥, 不直接覆盖整份可能已经更新的 authorized_keys。

官方依据