6.4 KiB
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 速度。
- 使用后正常安全弹出;程序不会重试用户文件写入,不能保证强拔/断网时的数据完整性。
回退
- 关闭 U 盘文件,在 Windows 正常安全弹出。
- PVE:
python3 /usr/local/libexec/vdi-usb-device-qmp.py detach --vm 100 --guest-ejected。 - 盒子:
sudo systemctl disable --now vdi-usb-monitor.service;PVE:systemctl disable --now vdi-usb-tunnel-100.service。 - 仅移除/恢复本功能 udev 规则,
udevadm control --reload-rules,再拔插让 Linux 本地重新识别。 - 如移除密钥,只删 authorized_keys 中
vdi-usb-poc-vm100对应公钥行;保留其他公钥, 不直接覆盖整份可能已经更新的 authorized_keys。