118 lines
6.4 KiB
Markdown
118 lines
6.4 KiB
Markdown
# 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/`](evidence/usb-device-20261103/)。
|
||
|
||
自动检查:盒子 Rust 测试 5/5、管理员 QMP 测试 3/3,通过;Python 编译检查和
|
||
`git diff --check` 通过。现场还实测了“本地挂载时拒绝导出”,随后正常卸载才导出。
|
||
|
||
## 实现
|
||
|
||
```text
|
||
盒子物理 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` 环境变量。部署器不保存密码。
|
||
|
||
```sh
|
||
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 执行:
|
||
|
||
```sh
|
||
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。
|
||
|
||
## 官方依据
|
||
|
||
- [usbredir 独立协议](https://www.spice-space.org/usbredir.html)
|
||
- [Ubuntu usbredirect](https://manpages.ubuntu.com/manpages/noble/man1/usbredirect.1.html)
|
||
- [QEMU USB 接入](https://www.qemu.org/docs/master/system/devices/usb.html)
|
||
- [QEMU filter 实现](https://github.com/qemu/qemu/blob/master/hw/usb/redirect.c)
|