EC20 搭建个人备用手机卡方案
备用手机卡这件事,平时存在感很低,但真到需要的时候又很麻烦。
比如一些账号还绑定在旧号码上,偶尔要收一条验证码;某张卡长期不用,又担心被运营商回收;或者出门在外时,家里的备用卡刚好收到一条重要短信。最原始的做法是把卡插在旧手机里长期充电,但这个方案并不稳定:手机会没电、系统会杀后台、远程查看短信也不方便。
这次我换了一个更偏 Homelab 的做法:把备用手机卡插到移远 EC20/EC25 这类 4G 模组里,接入家里的 Docker VM,通过 VoHive 管理短信和模组状态,再用青龙做定时保号提醒。
目标
这个方案要解决的问题很明确:
- 备用手机卡长期在线。
- 可以在浏览器里远程查看短信。
- 可以通过 API 发送短信。
- 有公网入口,但需要鉴权保护。
- 定期自动发一条保号短信,避免长期沉默。
- 整套服务要能被 Docker 管理和迁移。
最后形成的链路大概是这样:
SIM 卡
-> EC20/EC25 USB 模组
-> PVE 直通到 Docker VM
-> VoHive 管理短信和模组
-> Nginx Proxy Manager / Authelia 做入口保护
-> QingLong 定时调用 VoHive API 发保号短信硬件和系统结构
我这里的环境是家庭服务器上跑 PVE,里面有一台专门的 Docker VM。4G 模组通过 USB 接到 PVE,再直通给 Docker VM。
核心组件:
- PVE:负责虚拟机和 USB 设备直通
- Docker VM:运行实际服务
- EC20/EC25 模组:承载手机卡、短信和移动网络能力
- VoHive:提供短信收发、模组管理、Web UI 和 API
- QingLong:负责定时任务
- Nginx Proxy Manager + Authelia:负责公网入口和访问保护
模组识别正常时,在 Docker VM 里一般能看到类似设备:
ls -lah /dev/ttyUSB* /dev/cdc-wdm*
mmcli -L
lsusb常见表现是:
/dev/ttyUSB0
/dev/ttyUSB1
/dev/ttyUSB2
/dev/ttyUSB3
/dev/cdc-wdm0
QUECTEL Mobile Broadband Module这里不强依赖必须是 EC20。EC20、EC25 这类移远模组的整体思路类似,区别更多在制式、驱动细节和具体设备枚举上。
部署 VoHive
VoHive 的定位比较适合这个场景:它不是单纯的短信转发脚本,而是一个面向 EC20 / 高通 410 设备的管理平台,覆盖短信、VoWiFi、eSIM、模组状态和 API。
我的部署方式是单独放一个 Docker 目录:
/home/<user>/vohive-dockerdocker-compose.yml 可以保持得比较直接:
services:
vohive:
image: iniwex/vohive:latest
container_name: vohive
restart: unless-stopped
network_mode: host
privileged: true
volumes:
- ./config:/app/config
- ./data:/app/data
- ./logs:/app/logs
- /dev:/dev
environment:
- TZ=Asia/Shanghai
- CONFIG_PATH=/app/config/config.yaml这里有几个关键点:
network_mode: host:减少模组管理和本机网络探测的干扰。privileged: true:让容器能访问底层设备。/dev:/dev:把宿主机的 USB 串口、QMI 设备暴露给容器。./config、./data、./logs:配置、数据、日志都落在宿主机目录,方便备份和迁移。
启动:
cd /home/<user>/vohive-docker
docker compose up -d启动后先看容器状态和日志:
docker ps
docker logs -f vohive如果日志里出现过类似 QMI: initial sync failed,不一定就是致命问题。我的实际情况里它出现过,但服务仍然正常启动,Web UI 和短信接口都能使用。遇到这类日志时,优先看页面能否打开、设备是否在线、短信接口是否可用,不要只凭一行初始化日志下结论。
镜像拉取的坑
这次比较折腾的一点是镜像拉取。
国内环境下,直接拉 iniwex/vohive:latest 不一定顺利。我遇到过几类情况:
- Docker mirror 返回
403 Forbidden registry-1.docker.io直连超时- 某些代理源能解析 manifest,但下载 layer 时超时
- 换另一个代理源又返回未知错误
最后更稳的做法是借助可用的 HTTP 代理,用 regctl 先导出镜像 tar,再 docker load:
mkdir -p /home/<user>/bin /home/<user>/tmp
HTTP_PROXY=http://<proxy-host>:<proxy-port> \
HTTPS_PROXY=http://<proxy-host>:<proxy-port> \
curl -L -o /home/<user>/bin/regctl \
https://github.com/regclient/regclient/releases/download/v0.11.5/regctl-linux-amd64
chmod +x /home/<user>/bin/regctl
HTTP_PROXY=http://<proxy-host>:<proxy-port> \
HTTPS_PROXY=http://<proxy-host>:<proxy-port> \
/home/<user>/bin/regctl image export \
docker.io/iniwex/vohive:latest \
/home/<user>/tmp/vohive-latest.tar
docker load -i /home/<user>/tmp/vohive-latest.tar这个方式的好处是可控。普通 docker pull 失败时,错误经常比较粗;用 regctl image export 至少能更明确地知道卡在 manifest、layer 下载还是网络代理上。
Web 入口和访问保护
VoHive 管的是短信和手机卡,不建议裸奔在公网。
我的做法是:
- 内网直接访问 VoHive 的 Web UI。
- 公网通过 Nginx Proxy Manager 反代。
- 外层加 Authelia 认证。
- VoHive 自己仍然保留登录密码。
这样即使公网入口暴露,也至少有两层门:
浏览器
-> HTTPS 域名
-> Nginx Proxy Manager
-> Authelia
-> VoHive 登录如果只是内网使用,可以先不做公网入口。等短信收发、模组识别、API 调用都稳定后,再把公网链路接上。这样排查问题时会简单很多。
复刻基础能力:SimRelay
VoHive 能力比较完整,但它目前没有开放源代码。对个人备用卡这个场景来说,我并不一定需要完整的多设备、代理、eSIM、VoWiFi 能力,最核心的是这几件事:
- 能通过 EC20 的 AT 串口读取设备状态。
- 能读取收件箱、未读短信和已发送短信。
- 能发送中文短信。
- 能提供一个简单 Web 控制台。
- 能把这些能力封装成 HTTP API,方便后续接入青龙或其他自动化。
所以我后来按自己的需求复刻了部分基础功能,单独做了一个开源项目:SimRelay。
SimRelay 第一版优先支持单个移远 EC20 模组,通过 Linux USB 串口发送 AT 命令,默认使用 UCS2 文本短信模式来兼容中文短信。它不追求覆盖 VoHive 的完整能力,而是把“备用卡托管”最常用的短信收发、设备状态和 Web 控制台先做稳。
本地运行大概是这样:
./simrelay \
--device /dev/ttyUSB2 \
--baud 115200 \
--listen :7575 \
--timeout 5sDocker 部署时需要把 EC20 的 AT 串口映射进容器。如果还想补齐运营商、网络制式、频段、信号强度等信息,也可以把 QMI 设备映射进去:
services:
simrelay:
image: simrelay:latest
container_name: simrelay
restart: unless-stopped
environment:
TZ: Asia/Shanghai
SIMRELAY_DEVICE: /dev/ttyUSB3
SIMRELAY_BAUD: "115200"
SIMRELAY_LISTEN: ":7575"
SIMRELAY_TIMEOUT: 5s
SIMRELAY_ADMIN_USERNAME: admin
SIMRELAY_ADMIN_PASSWORD: ${SIMRELAY_ADMIN_PASSWORD:-change-me}
SIMRELAY_QMI_DEVICE: /dev/cdc-wdm0
devices:
- /dev/ttyUSB3:/dev/ttyUSB3
- /dev/cdc-wdm0:/dev/cdc-wdm0
ports:
- "7576:7575"它和 VoHive 的关系更像是轻量替代方案,而不是完全复刻。VoHive 适合作为完整管理平台;SimRelay 更适合“我只想稳定托管一张备用卡,并能自己控制代码”的场景。
用 API 发短信
VoHive 的接口文档可以直接通过 Web UI 查看,核心是两步:
- 登录拿 token。
- 带上
Authorization: Bearer <token>调用短信发送接口。
登录接口:
POST /api/auth/login
Content-Type: application/json请求体:
{
"username": "<username>",
"password": "<password>"
}返回里会有 token。
发送短信接口:
POST /api/sms/send
Authorization: Bearer <token>
Content-Type: application/json请求体最少只需要两个字段:
{
"phone": "+10000000000",
"message": "test message"
}如果有多张卡或多个模组,也可以传 device_id 或 imsi 来指定设备。
青龙做定时保号
备用卡最怕的是长期不用。很多运营商虽然规则不同,但保持周期性通信总归更稳。
我用青龙做了一个很简单的定时任务:每四个月发一条短信。比如每年 1 月 / 5 月 / 9 月 的 1 号执行一次:
1 1 1 1,5,9 *任务命令:
python3 /ql/data/scripts/vohive-send-keepalive-sms.py脚本逻辑非常简单:
- 读取 VoHive 地址、账号、密码、目标手机号和短信内容。
- 调用
/api/auth/login获取 token。 - 调用
/api/sms/send发送短信。 - 打印发送结果,方便在青龙日志里确认。
为了避免把配置写死,脚本可以通过环境变量覆盖:
VOHIVE_BASE_URL
VOHIVE_USERNAME
VOHIVE_PASSWORD
VOHIVE_SMS_PHONE
VOHIVE_SMS_MESSAGE
VOHIVE_DEVICE_ID
VOHIVE_IMSI实际维护时有一个小坑:青龙的“定时任务”和“脚本管理”不是一回事。
定时任务只保存命令和 cron;脚本管理展示的是 /ql/data/scripts 目录里的文件。如果只通过 API 创建了定时任务,但脚本文件没有同步到青龙容器的 /ql/data/scripts,那么任务列表里能看到任务,脚本管理里却看不到脚本。
所以正确顺序应该是:
先把脚本放进 /ql/data/scripts
再创建青龙定时任务
最后手动执行一次验证日志验证命令:
docker exec qinglong python3 /ql/data/scripts/vohive-send-keepalive-sms.py如果返回里能看到类似 status=ok、delivery_state=acked,说明 VoHive 已经接受发送请求。
备份和维护
这个方案里真正重要的不是容器,而是配置和数据目录。
建议至少备份这些路径:
vohive-docker/config
vohive-docker/data
vohive-docker/logs
qinglong-docker/data日常维护可以关注几件事:
- 模组重启后是否还能识别到
/dev/ttyUSB*和/dev/cdc-wdm* - VoHive 页面里设备是否在线
- 青龙定时任务是否仍然启用
- 保号短信是否有执行日志
- 公网入口是否仍然经过认证保护
如果更换 USB 口、调整 PVE 直通或重启 Docker VM,优先检查设备枚举是否变化。模组类设备最常见的问题不是应用挂了,而是底层设备没有正确交给容器。
总结
这套方案本质上是把一张“只能插在手机里的备用卡”,变成一个可管理的家庭基础设施:
- EC20/EC25 负责移动网络和短信能力。
- Docker 负责运行环境。
- VoHive 或 SimRelay 负责 Web UI 和 API。
- NPM + Authelia 负责入口保护。
- QingLong 负责周期性自动化。
它不一定比旧手机方案更简单,但更可控、更容易备份,也更适合放进 Homelab 的统一运维体系里。对备用卡这种低频但关键的东西来说,稳定在线、能远程查看、能自动保号,比功能花哨更重要。