目前飞牛USB直通简单地用usb设备号作为设备区分标志,插两个同型号的usb设备100%就会出现这个网友所遇到的问题(硬件直通添加usb无法启动虚拟机
https://club.fnnas.com/forum.php?mod=viewthread&tid=40334
(出处: 飞牛私有云论坛 fnOS)),原因是虚拟机没发区分哪个是哪个usb设备,直接就报错。
所以建议飞牛开发大佬能够将虚拟机USB直通改变单一的VID和PID识别设备方法,增加bus号,解决同型号多设备的usb直通问题。
我让AI临时帮我解决了,但是普通网友的动手能力肯定不够的。希望官方出手解决。
全新飞牛 + iStoreOS 多 4G 模块 USB 直通 · 从零跑通手册
目标:在一台全新飞牛上,把多个同型号 4G 模块(如 EC20/EC25,VID:PID 相同)直通进 iStoreOS 虚拟机,实现开机自动挂载、拔插自由、GUI 不报错。照本手册阶段 0→8 一遍跑通。
适用终端:MobaXterm(多行粘贴正常)。脚本创建首选 MobaXterm 左侧 SFTP 上传。
工具与粘贴约定(先读,省后面所有坑)
- 本教程命令按 MobaXterm 编写,多行命令可整段复制粘贴,一次回车执行。
- 创建脚本文件有两条路,强烈推荐路线甲:
- 路线甲(首选,零风险):在 Windows 用任意编辑器(记事本/VSCode/Notepad++)把脚本内容存成文件 → 用 MobaXterm 左侧 SFTP 面板拖到目标目录 → 回终端
chmod +x。MobaXterm 连上 SSH 后左侧自动就是 SFTP,在左侧上方路径框输入 /usr/local/bin 或 /etc/libvirt/hooks 回车即可定位,直接把文件拖进去。完全不经过终端粘贴,不可能出错。
- 路线乙(备选):终端
nano 路径 → 粘贴内容 → Ctrl+O 回车 → Ctrl+X。粘完必须 wc -l 路径 看行数,跟教程给的行数对得上才算成功;对不上说明你的终端/配置吞了换行,退回路线甲。
- 本文不再使用 base64(base64 只适合“原样复制某台现成机器”,不适合“通用全新教程”,因为 VM 名/VID/PID 都要改)。
原理 30 秒(重建时心里有底)
- 同型号冲突的根因:libvirt 按
vendor/product 在 XML 里声明直通时,分不清两个一模一样的设备,报 Multiple USB devices ... use <address> to specify one。
- 本方案的解法:开机时不在 XML 里写任何 USB 直通,虚拟机先空着启动(永远能起,不会报 Multiple);启动完成后由 libvirt hook 触发脚本,脚本实时读 sysfs 找出当前在线的、VID/PID 匹配的模块,拿到每个设备真实的
bus/device 号,逐个 attach-device 热挂。插几个挂几个、拔了的不挂也不报错。
- 关键简化:因为不在 XML 里硬绑定,你不需要记
bus/port/device,只需要模块的 VID 和 PID 两个值。
- USB 直通不需要 IOMMU / VT-d(那是 PCIe 直通才要的)。USB 走 managed hostdev + usbfs,只要 CPU 虚拟化(VT-x/AMD-V)开了即可。别去折腾 IOMMU,白费功夫。
managed='yes' 的边界(血的教训):它假定设备占用/释放全程由 libvirt 控制。虚拟机运行时任何让 USB“断开→重连”的事件(物理拔模块 / 接触不良 / 客户机里发 AT+CFUN=1,1 软重启)都会污染 libvirtd 内存,导致后续挂不上、甚至连累另一个模块。所以:动 USB 必须在关机窗口;要复位模块用“重启虚拟机”代替 AT 软重启。
- GUI 红字
invalid argument 的来源:只有两种——① 绕过 GUI 用命令行改过配置导致 GUI 账本对不上;② libvirtd 被重启(系统更新/自救命令)后飞牛后端连接断了。全新环境按本教程规矩来,①不会发生;②万一遇到,第 8 节一条命令修好,不影响虚拟机。
阶段 0:硬件与 BIOS 准备
- BIOS 开 CPU 虚拟化:Intel 开
Intel VT-x(Virtualization Technology),AMD 开 SVM Mode。不开则 kvm 起不来,虚拟机建不了。
- 规划 USB 插口:把要直通的 4G 模块插到固定的物理 USB 口上,插好后尽量别换口、别换集线器。两个模块插哪两个口都行(hook 方案不依赖具体口,但固定插口能让 device 号更稳定、减少意外)。
- 模块先插好、上电,再开始后面的操作。
阶段 1:飞牛初始化 + 安装虚拟机功能
飞牛 GUI 路径随版本略有差异,下面给“GUI 操作 + 命令行验证”双保险,命令行能确认组件在位即可,不必纠结 GUI 菜单名。
-
GUI 安装虚拟机功能:打开飞牛 → 应用中心/套件中心 → 安装并启用「虚拟机」应用 → 首次打开让它完成初始化(会部署 libvirt/qemu)。
-
命令行验证组件在位(MobaXterm 连上飞牛 SSH,逐条执行):
which qemu-system-x86_64 # 应输出 /usr/bin/qemu-system-x86_64
systemctl is-active libvirtd # 应输出 active
lsmod | grep -E 'kvm_intel|kvm_amd' # 应有一行 kvm_intel 或 kvm_amd
- 若
lsmod 为空:说明虚拟化模块没加载,先 modprobe kvm_intel(AMD 用 modprobe kvm_amd);若报错,回 BIOS 确认 VT-x/AMD-V 已开。
- 若
qemu-system-x86_64 不存在:说明虚拟机套件没装好,回 GUI 重装/启用「虚拟机」。
-
(可选确认)virsh version 能正常输出版本号,说明 libvirt 客户端可用。
阶段 2:准备 iStoreOS 镜像 + 飞牛 GUI 建机 + 装系统
-
下载 iStoreOS 镜像:到 iStoreOS 官方发布页下载 x86-64 版本(combined 或 generic,格式可能是 .img.gz / .img / .qcow2)。
-
格式转换(按需):飞牛建机一般要 qcow2。若你下的是 .img.gz,先解压再转换:
gunzip -k xxx.img.gz # 得到 xxx.img
qemu-img convert -f raw -O qcow2 xxx.img /vol1/xxx.qcow2 # 转成 qcow2,放到数据盘
若下载的就是 .qcow2,跳过转换,直接上传到飞牛数据盘某目录备用。
-
飞牛 GUI 建机(⚠️ 关键纪律):
- 新建虚拟机,磁盘指向第 2 步的 qcow2(不要新建空盘,除非你要从 ISO 装 iStoreOS)。
- 规格参考:CPU 1 核、内存 1024 MB、磁盘够用即可。
- 网卡选「桥接」,桥接到你飞牛 LAN 口对应的桥(飞牛里常是
br0 或某个 ovs 桥,按你环境选;选桥接 iStoreOS 才能拿到局域网 IP,方便后面 SSH 进去装驱动)。
- ⚠️ 建机过程中以及之后,绝不要在 GUI 里添加任何 USB 直通设备。 这是本方案 GUI 不红字、不冲突的前提。
- 记下建出来的虚拟机名(飞牛常给随机名,下文用
VMNAME 代指,你自己替换;本文示例沿用 t08lhu72)。
-
启动虚拟机、装/进入 iStoreOS:GUI 点启动,打开控制台(或命令行 virsh console VMNAME,退出按 Ctrl 再按 ])。若是 combined 镜像,启动即进系统;若需安装,按 iStoreOS 提示装到虚拟磁盘。
⚠️ iStoreOS 联网 IP 坑(OpenWrt 系经典,全新必遇):iStoreOS 默认 LAN IP 常是 192.168.1.1 或 10.0.0.1,可能跟你家局域网不在同网段、或与飞牛/路由器冲突,导致浏览器打不开 LuCI、也 ping 不通。
解决:用 virsh console VMNAME 进 iStoreOS 命令行,编辑 /etc/config/network 里 config interface 'lan' 的 option ipaddr,改成你家局域网里一个空闲且同网段的 IP(如 192.168.x.200),保存后 /etc/init.d/network restart(或直接重启虚拟机)。改完用浏览器访问新 IP 进 LuCI,并确认 iStoreOS 能上外网(ping -c 3 8.8.8.8 通),下一步装包要用外网。
阶段 3:iStoreOS 内装 4G 驱动包
此时 4G 模块还归宿主机、没直通,iStoreOS 里看不到设备是正常的;但驱动包可以提前装好,等阶段 5 直通后设备一进来就自动识别。
- 浏览器进 iStoreOS 的 LuCI → 系统 → 软件包 → 更新列表(
opkg update,需上一步外网已通)。
- 搜索并安装以下包(包名以 iStoreOS 软件源实际为准,OpenWrt 通用名如下;LuCI 搜
modem/qmi/option 也能找到):
kmod-usb-serial-option(串口驱动,产生 ttyUSB*)
kmod-usb-serial-wwan
kmod-usb-net-qmi-wwan(QMI 网卡驱动,产生 wwan* / cdc-wdm*)
libqmi-utils(提供 qmicli 拨号工具)
luci-proto-qmi(LuCI 图形化 QMI 拨号,推荐)
- 可选:
luci-app-mwan3 + mwan3(后面做双线负载均衡用)
- 装完先不用重启,进入阶段 4、5;直通后设备进来驱动会自动绑定。
阶段 4:查模块 VID/PID + 在宿主机建 hook 两件套
4.1 查你自己模块的 VID/PID(填进脚本用)
在飞牛终端执行:
lsusb | grep -i -E 'quectel|2c7c|lte|modem'
看输出形如 ID 2c7c:0125 Quectel ... EC25 ...,冒号前是 VID、冒号后是 PID。把这两个值记下(EC20/EC25 通常就是 2c7c / 0125,但以你机器 lsusb 实际为准)。
若一行都 grep 不到,说明模块没被宿主机识别:检查插口/供电,或 lsusb 看全部设备找你的模块型号。
4.2 创建核心脚本 attach-ec25.sh
把下面整段存成文件 attach-ec25.sh(Windows 编辑器存,注意把开头三行 VM/VID/PID 改成你自己的值):
#!/bin/sh
# attach-ec25.sh —— 把当前在线的 4G 模块热挂进虚拟机
# 无参数:挂所有在线模块(开机 hook 用)
# 两个参数 <bus> <dev>:只挂这一个(udev 用,主线可不用)
#
# ★★★ 改成你自己的值(VID/PID 见 4.1;VM 见阶段 2 记下的虚拟机名)★★★
VM="t08lhu72"
VID="2c7c"
PID="0125"
# ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
LOG=/var/log/attach-ec25.log
DONE=/var/run/attach-ec25.done
MAX_RETRY=5
# 日志超 512K 自动保留尾部 200 行
if [ -f "$LOG" ] && [ "$(stat -c%s "$LOG" 2>/dev/null || echo 0)" -gt 524288 ]; then
tail -n 200 "$LOG" > "$LOG.tmp" && mv -f "$LOG.tmp" "$LOG"
fi
log(){ echo "$(date '+%F %T') $*" >> "$LOG"; }
norm(){ printf '%d' "$1" 2>/dev/null; }
is_attached(){ grep -qxF "$1:$2" "$DONE" 2>/dev/null; }
mark_attached(){ echo "$1:$2" >> "$DONE"; }
attach_one(){
case "$1$2" in ''|*[!0-9]*) log "skip bad bus/dev: $1 $2"; return 1;; esac
b=$(norm "$1"); d=$(norm "$2")
if is_attached "$b" "$d"; then log "skip already-done bus=$b device=$d"; return 0; fi
f="/tmp/ec25_${b}_${d}.xml"
cat > "$f" <<EOF
<hostdev mode='subsystem' type='usb' managed='yes'>
<source><address bus='$b' device='$d'/></source>
</hostdev>
EOF
i=0
while [ "$i" -lt "$MAX_RETRY" ]; do
if virsh attach-device "$VM" "$f" >>"$LOG" 2>&1; then
log "OK attached bus=$b device=$d"; mark_attached "$b" "$d"; return 0
fi
i=$((i+1)); sleep 2
done
log "FAIL attach bus=$b device=$d after $MAX_RETRY tries"; return 1
}
log "=== run start args=[$*] ==="
if ! virsh domstate "$VM" 2>/dev/null | grep -q running; then
log "VM not running, exit"; exit 0
fi
if [ "$#" -ge 2 ]; then
attach_one "$1" "$2"
else
: > "$DONE"
found=0
if [ -d /sys/bus/usb/devices ]; then
for devdir in /sys/bus/usb/devices/*/; do
devname="${devdir%/}"; devname="${devname##*/}"
case "$devname" in *:*) continue;; esac
[ -r "${devdir}idVendor" ] || continue
[ "$(cat "${devdir}idVendor" 2>/dev/null)" = "$VID" ] || continue
[ "$(cat "${devdir}idProduct" 2>/dev/null)" = "$PID" ] || continue
[ -r "${devdir}busnum" ] && [ -r "${devdir}devnum" ] || continue
attach_one "$(cat "${devdir}busnum")" "$(cat "${devdir}devnum")"; found=1
done
fi
if [ "$found" = 0 ] && command -v lsusb >/dev/null 2>&1; then
log "sysfs found nothing, fallback lsusb"
lsusb -d "${VID}:${PID}" 2>/dev/null | awk '{gsub(/:/,"",$4); print $2, $4}' \
| while read b d; do attach_one "$b" "$d"; done
fi
fi
log "=== run end ==="
4.3 创建 hook 脚本 qemu
把下面整段存成文件 qemu(文件名就叫 qemu,不要加 .sh;把里面 t08lhu72 改成你的虚拟机名,必须和 4.2 的 VM 一致):
#!/bin/sh
# libvirt qemu hook:记录每次调用;虚拟机 started 后后台热挂 4G 模块
GUEST="$1"; ACTION="$2"; SUB="$3"
echo "$(date '+%F %T') hook guest=$GUEST action=$ACTION sub=$SUB" >> /var/log/libvirt-hook-qemu.log
if [ "$GUEST" = "t08lhu72" ] && [ "$ACTION" = "started" ]; then
nohup /usr/local/bin/attach-ec25.sh >/dev/null 2>&1 &
fi
exit 0
4.4 上传 + 赋权 + 自检
路线甲(SFTP 上传):MobaXterm 左侧路径框分别输入 /usr/local/bin 和 /etc/libvirt/hooks(后者若不存在,先在终端 mkdir -p /etc/libvirt/hooks),把 attach-ec25.sh 拖进前者、qemu 拖进后者。然后回终端:
chmod +x /usr/local/bin/attach-ec25.sh /etc/libvirt/hooks/qemu
路线乙(nano 粘贴):nano /usr/local/bin/attach-ec25.sh 粘 4.2 内容保存;mkdir -p /etc/libvirt/hooks && nano /etc/libvirt/hooks/qemu 粘 4.3 内容保存;再执行上面 chmod。
自检(应回显 syntax OK,行数约 67 / 8):
sh -n /usr/local/bin/attach-ec25.sh && sh -n /etc/libvirt/hooks/qemu && echo "syntax OK"
wc -l /usr/local/bin/attach-ec25.sh /etc/libvirt/hooks/qemu
若 sh -n 报语法错,几乎一定是文件被吞了换行/带了 Windows 换行符,执行 sed -i 's/\r$//' /usr/local/bin/attach-ec25.sh /etc/libvirt/hooks/qemu 后重测;仍不行就改用路线甲 SFTP 重传。
阶段 5:确认无硬绑定 + 开机自启 + 触发挂载 + 验证
-
确认虚拟机定义里没有 USB 直通(全新建机本来就没有,确认一下,防止手滑在 GUI 加过):
virsh dumpxml VMNAME | grep -c hostdev # 应输出 0
若不为 0,说明你在 GUI 加过直通,回 GUI 把 USB 设备删掉,或用 virsh edit VMNAME 删掉 <hostdev>...</hostdev> 段。
-
开启开机自启:
virsh autostart VMNAME
-
重启虚拟机触发 hook(让刚建的 hook 生效,挂上模块):
virsh destroy VMNAME; sleep 2; virsh start VMNAME; sleep 12
-
看日志:
echo "==hook=="; tail -n 6 /var/log/libvirt-hook-qemu.log
echo "==attach=="; tail -n 12 /var/log/attach-ec25.log
预期:hook 日志有本次时间戳的 action=started;attach 日志有 === run start === + 每个在线模块一条 OK attached bus=3 device=... + === run end ===。
阶段 6:iStoreOS 内验证设备 + 拨号上线
SSH 或 LuCI 进 iStoreOS:
lsusb | grep -i -E 'quectel|2c7c' # 行数应等于模块数
ls /dev/ttyUSB* # 每模块约 4 个串口
ls /dev/cdc-wdm* # 个数应等于模块数
ip link | grep wwan # wwan0 / wwan1 ...
数字对得上 = 直通成功且驱动加载完毕。
拨号(每个模块一条线),二选一:
-
LuCI 图形化(推荐):网络 → 接口 → 添加,协议选 QMI,设备填 /dev/cdc-wdm0(第二条线填 cdc-wdm1),填 APN(移动 cmnet / 联通 3gnet / 电信 ctnet),保存应用。第二条线同理再建一个接口。
-
命令行:
echo Y > /sys/class/net/wwan0/qmi/raw_ip # QMI 必开 raw_ip,否则拿不到 IP
ip link set wwan0 up
qmicli -d /dev/cdc-wdm0 --wds-start-network="apn=cmnet,ip-type=4" --client-no-release-cid
qmicli -d /dev/cdc-wdm0 --wds-follow-network &
udhcpc -i wwan0
第二条线把命令里的 0 全换成 1。
两线都 UP 后,若要双线负载/故障转移,在 mwan3 里把两个接口加入策略即可。
阶段 7:整机 reboot 验证开机自挂(最终关)
之前阶段 5 只验证了“重启虚拟机”。整机重启时 libvirtd/USB/驱动/hook 的启动顺序最乱,是最容易翻车的一关,必须验一次:
reboot
等飞牛完全起来、虚拟机自启后,SSH 回去:
virsh list --all # VMNAME 应为 running
tail -n 12 /var/log/attach-ec25.log # 应有本次开机的 run start + 每条 OK + run end
再进 iStoreOS 看 lsusb | grep 2c7c 行数对得上。这一关过了,整套方案在任何启动方式下都稳了。
阶段 8:日常纪律 + 急救 + GUI 红字处理
8.1 日常操作纪律
| 想做的事 |
正确动作 |
禁止 |
| 拔/插整个 4G 模块 |
先 virsh destroy VMNAME,动手,再 virsh start VMNAME |
运行中拔模块本体 |
| 换 SIM 卡(只抽卡、不拔模块) |
开机直接换没事;换完多数自动认,需重拨就 ifdown/ifup 该线;换运营商改 APN |
运行中拔模块本体 |
| 模块抽风/换卡不认想复位 |
virsh destroy + start(=干净重启模块) |
客户机内发 AT+CFUN=1,1 |
改 APN/查信号/qmicli 看卡状态 |
直接做,不动 USB 连接,安全 |
— |
8.2 急救:设备挂不上 / 误运行中拔过模块
单行整粘(清 libvirtd 内存脏状态 + 重置模块 + 重挂;把 2c7c 换成你的 VID):
virsh destroy VMNAME 2>/dev/null; systemctl restart libvirtd; for d in /sys/bus/usb/devices/*/; do [ "$(cat ${d}idVendor 2>/dev/null)" = "2c7c" ] || continue; echo 0 > ${d}authorized 2>/dev/null; sleep 1; echo 1 > ${d}authorized 2>/dev/null; done; sleep 3; virsh start VMNAME; sleep 12; tail -n 8 /var/log/attach-ec25.log
8.3 GUI 红字 Error: invalid argument(不影响 VM/SSH)
先精准:
systemctl restart trim_sac.service trim_http_cgi.service; sleep 3; systemctl is-active trim_sac.service trim_http_cgi.service
不行则 systemctl restart trim_main.service trim_nginx.service;再不行 reboot。处理后用浏览器无痕窗口测 GUI(排除缓存)。
8.4 命令行控制面板(GUI 不可用时也能完全控制)
virsh list --all
virsh shutdown VMNAME # 优雅关机
virsh destroy VMNAME # 强制断 电
virsh start VMNAME # 开机
virsh console VMNAME # 进 iStoreOS 终端,退出按 Ctrl 再按 ]
virsh vncdisplay VMNAME # 返回 :N 则用 VNC 客户端连 飞牛IP:590N 看画面
8.5 改配置前先备份
cp /etc/libvirt/qemu/VMNAME.xml /root/VMNAME_$(date +%F_%H%M).xml
系统更新与重建影响速查
| 事件 |
影响 |
处理 |
| 飞牛小更新/补丁 |
hook 不受影响;可能因 libvirtd 重启致 GUI 短暂红字 |
红字→8.3 |
| 飞牛升级 libvirt/qemu 包 |
hook 文件不受影响;可能红字 |
红字→8.3 |
飞牛大版本/重装系统盘(/vol1 数据盘在) |
hook 两件套丢失;qcow2 在,虚拟机不用重装 |
重做阶段 4、5(建机定义若丢则先 GUI 指向旧 qcow2 重建) |
| 飞牛换机/数据盘丢 |
全丢 |
阶段 0→8 全跑 |
| iStoreOS 升级(保留配置) |
无影响 |
通常无需操作 |
| iStoreOS 升级(不保留配置)/重置/重装 |
驱动包与拨号丢 |
重做阶段 3、6 |
红线清单(置顶 memorize)
- 绝不在飞牛 GUI 给这台机添加 USB 直通——它的「硬件/直通」栏显示为空是正确的,真实直通靠 hook 热挂。一旦在 GUI 加,立刻回到同型号冲突老路。
- 别删
/usr/local/bin/attach-ec25.sh 与 /etc/libvirt/hooks/qemu,它们是开机自挂本体。
- 别在 GUI 点删除/移除该虚拟机,尤其命令行改过配置后;要改先按 8.5 备份。
- 运行中别拔模块本体、别发
AT+CFUN=1,1;动 USB 必在关机窗口,复位用重启虚拟机。
- 脚本里
VM/VID/PID 三处(attach 脚本三处 + hook 脚本一处 VM)必须与你环境一致,否则 hook 过滤不到、不挂设备。
- 创建脚本首选 MobaXterm SFTP 上传;用 nano 粘则粘完
wc -l 看行数,并对 sh -n 语法检查。
照本手册,全新飞牛从阶段 0 走到阶段 7 即可稳定服役;日后无论飞牛还是 iStoreOS 出问题,按“影响速查表”定位到对应阶段重做即可一遍跑通。👍