功能:增加终端USB设备监控POC

This commit is contained in:
songyiming committed 2026-11-03 17:48:01 +08:00
1 parent 1bed1948b5
commit a98d9ddeee
15 files changed
+868

No files matched your search

@@ -0,0 +1,28 @@
active
enabled
11月 03 17:27:23 rk3528pro-ddr4 systemd[1]: Started vdi-usb-monitor.service - VDI approved USB storage redirection (device POC).
11月 03 17:27:23 rk3528pro-ddr4 vdi-usb-monitor[56130]: USB filesystem is mounted locally; unmount safely before export
11月 03 17:27:25 rk3528pro-ddr4 vdi-usb-monitor[56130]: exporting approved storage 3-1 at 127.0.0.1:40001
11月 03 17:29:02 rk3528pro-ddr4 vdi-usb-monitor[56130]: export session ended: exit status: 0; waiting before retry
11月 03 17:29:04 rk3528pro-ddr4 vdi-usb-monitor[56130]: exporting approved storage 3-1 at 127.0.0.1:40001
11月 03 17:34:43 rk3528pro-ddr4 vdi-usb-monitor[56130]: approved USB device disconnected; waiting for reinsertion
11月 03 17:34:45 rk3528pro-ddr4 vdi-usb-monitor[56130]: waiting for exactly one approved device; found 0
11月 03 17:34:53 rk3528pro-ddr4 vdi-usb-monitor[56130]: exporting approved storage 3-1 at 127.0.0.1:40001
/: Bus 001.Port 001: Dev 001, Class=root_hub, Driver=xhci-hcd/1p, 480M
|__ Port 001: Dev 047, If 0, Class=Human Interface Device, Driver=usbhid, 1.5M
/: Bus 002.Port 001: Dev 001, Class=root_hub, Driver=xhci-hcd/1p, 5000M
/: Bus 003.Port 001: Dev 001, Class=root_hub, Driver=ehci-platform/1p, 480M
|__ Port 001: Dev 006, If 0, Class=Mass Storage, Driver=usbfs, 480M
/: Bus 004.Port 001: Dev 001, Class=root_hub, Driver=ohci-platform/1p, 12M
NAME TRAN FSTYPE LABEL MOUNTPOINTS
mmcblk1 mmc
├─mmcblk1p1 mmc ext4 armbi_boot /boot
└─mmcblk1p2 mmc ext4 armbi_root /var/log.hdd
/
mmcblk1boot0 mmc
mmcblk1boot1 mmc
zram0 [SWAP]
zram1 /var/log
zram2
active
46934 /home/armbian/vdi-build/rkmpp-experiment/build/moonlight/app/moonlight stream 192.168.1.70 Desktop --resolution 2560x1440 --fps 45 --bitrate 30000 --display-mode fullscreen --video-codec H.264 --video-decoder hardware --absolute-mouse --keep-awake --no-vsync --no-frame-pacing --audio-on-host
@@ -0,0 +1,16 @@
{
"vm": 100,
"device_present": true,
"channels": [
{
"frontend-open": true,
"filename": "tcp:127.0.0.1:45882 <-> 127.0.0.1:40001",
"label": "vdi_usb_box_100"
}
],
"usb": " Device 0.1, Port 1, Speed 480 Mb/s, Product QEMU USB Tablet, ID: tablet\r\n Device 1.1, Port 2, Speed 480 Mb/s, Product USB Redirection Device, ID: vdi_usb_redir_100\r\n",
"vm_status": {
"status": "running",
"running": true
}
}
@@ -0,0 +1,49 @@
{␍
"USB": [␍
{␍
"Model": "Kingston DataTraveler 3.0 USB Device",␍
"InterfaceType": "USB",␍
"PNPDeviceID": "USBSTOR\\DISK\u0026VEN_KINGSTON\u0026PROD_DATATRAVELER_3.0\u0026REV_PMAP\\408D5C1653D4E44179789251\u00260",␍
"Size": 62051512320,␍
"Index": 1␍
}␍
],␍
"Volume": [␍
{␍
"DriveLetter": "F",␍
"FileSystemLabel": "KINGSTON",␍
"FileSystem": "FAT32",␍
"DriveType": "Removable",␍
"Size": 61908123648,␍
"HealthStatus": "Healthy"␍
}␍
],␍
"PnP": [␍
{␍
"Status": "OK",␍
"Class": "DiskDrive",␍
"FriendlyName": "Kingston DataTraveler 3.0 USB Device",␍
"InstanceId": "USBSTOR\\DISK\u0026VEN_KINGSTON\u0026PROD_DATATRAVELER_3.0\u0026REV_PMAP\\408D5C1653D4E44179789251\u00260"␍
},␍
{␍
"Status": "OK",␍
"Class": "USB",␍
"FriendlyName": "USB Mass Storage Device",␍
"InstanceId": "USB\\VID_0951\u0026PID_1666\\408D5C1653D4E44179789251"␍
}␍
],␍
"GPU": [␍
{␍
"Name": "Virtual Display Driver",␍
"ConfigManagerErrorCode": 0␍
},␍
{␍
"Name": "NVIDIA GeForce GTX 965M",␍
"ConfigManagerErrorCode": 0␍
},␍
{␍
"Name": "Microsoft Basic Display Adapter",␍
"ConfigManagerErrorCode": 0␍
}␍
]␍
}␍
@@ -0,0 +1,21 @@
{␍
"Result": "PASS",␍
"Disk": [␍
{␍
"Model": "Kingston DataTraveler 3.0 USB Device",␍
"InterfaceType": "USB",␍
"PNPDeviceID": "USBSTOR\\DISK\u0026VEN_KINGSTON\u0026PROD_DATATRAVELER_3.0\u0026REV_PMAP\\408D5C1653D4E44179789251\u00260",␍
"Index": 1␍
}␍
],␍
"Volume": [␍
{␍
"DriveLetter": "F",␍
"DriveType": "Removable",␍
"FileSystemLabel": "KINGSTON",␍
"FileSystem": "FAT32",␍
"HealthStatus": "Healthy"␍
}␍
],␍
"SHA256": "8244F1189EFA02118BF0DC4CB68FC8924FE562AC610B68DEEFBE09EC85454D51"␍
}␍
@@ -0,0 +1,11 @@
{␍
"Result": "PASS",␍
"Model": "Kingston DataTraveler 3.0 USB Device",␍
"InterfaceType": "USB",␍
"PNPDeviceID": "USBSTOR\\DISK\u0026VEN_KINGSTON\u0026PROD_DATATRAVELER_3.0\u0026REV_PMAP\\408D5C1653D4E44179789251\u00260",␍
"DriveLetter": "F",␍
"TestDirectory": "F:\\VDI_USB_TEST_20261103-173342",␍
"LocalDirectory": "C:\\Windows\\TEMP\\VDI_USB_TEST_20261103-173342",␍
"Bytes": 1048576,␍
"SHA256": "8244F1189EFA02118BF0DC4CB68FC8924FE562AC610B68DEEFBE09EC85454D51"␍
}␍
+117
View File
@@ -0,0 +1,117 @@
# 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)