前言です
事情的起因非常简单:作者在某鱼上花了一杯奶茶钱,淘到一块从旧 CPE(运营商送的 4G 家用路由)里拆出来的 mSATA 模块——ZTE MF253S,方案 ZX297510。
想法也很朴素:插进电脑的 mSATA 槽,当一张原生 4G 网卡用。能用就用,不能用就扔。
しかし……嘻嘻嘻(千早爱音唐笑),这东西不仅不能用,还会让你怀疑人生:插上去,开机 21 秒,它准时重启一次,像极了到点下班的本社畜;ModemManager 一碰它,它直接连人带机跑进 BootROM,连尸体都懒得留。
为什么呢?因为这块模块的固件是给运营商 CPE 定制的,压根没打算让你当普通网卡用。作者翻遍了恩山、读完了 ModemManager 的插件源码、把它家 CPE 的日志都掏出来看了一遍——最后给它写了个内核驱动。现在它已经能在装 stock ModemManager / NetworkManager 的机器上即插即用:任务栏直接弹出「移动数据」,跟正经的 EC20 一个待遇。
这篇文章就把整个折腾过程从原理到代码讲清楚。
硬件参数
| 项目 | 详情 |
|---|---|
| 模块型号 | ZTE MF253S / ME3760V2 |
| 基带方案 | ZTE Sanechips ZX297510(WF7510) |
| 接口 | mini-PCIe / mSATA(USB 2.0 信号) |
| USB ID | 19d2:0199(正常模式)/ 19d2:0256(BootROM) |
| 网络制式 | TD-LTE(移动定制,硬件上只校准了 GSM / TD-SCDMA / LTE,没有 WCDMA) |
| USB 接口布局 | if0 = USB-AT,if1 = USB-Rndis(ECM 数据),if2 = USB-Modem,if3 = USB-log,if4 = USB-adb |
划重点:if1 是 ECM 数据口,但它不是标准 CDC ECM,主机侧需要自己写驱动绑定。
适用人群
会使用 Linux,能自己编译内核模块。此教程并不适合小白——如果你只是想插卡上网,建议直接买正经的 EC20 / RM500U。
准备材料
- ZTE MF253S / ME3760V2 模块一块
- 带 mSATA 槽的电脑一台(别用 mSATA 转 USB 的板子,容易供电不足)
- 极强的动手 / 思考 / 搜索能力
- AI 一位(这次用的是 DeepSeek V4.1 Flash,能啃几万行 C 代码,比作者自己靠谱多了)
- 良好的网络环境(你知道我在说什么 👁)
为什么这么难搞(原理)
先说结论:这块模块的固件,从设计上就没考虑过「标准主机栈」这件事。作者踩过的坑依次如下:
- QMI 是死路。
qmicli --dms-get-manufacturer能通,但任何 UIM / NAS 类命令都会把它搞崩——直接掉进 BootROM 模式重启。ModemManager 如果按 QMI 去探它,就是无限的 21 秒重启循环。 - PPP 拨号被拒绝。LTE 的默认承载在 attach 时自动激活,此时再发
ATD*99***1#,模块直接回+CME ERROR: 3。想用 PPP?只有刚做完 CFUN 循环的那几秒能用,等于没用。 - 没有 DHCP。ECM 网卡口不跑 DHCP 服务端。IP 地址、网关、DNS 只在私有 URC
+ZGIPDNS里出现,长这样:
+ZGIPDNS: 1,1,"IP","10.x.x.x","0.0.0.0","36.x.x.x","36.x.x.x"
其中网关字段恒为0.0.0.0,需要自己推导成「IP+1」。 - 数据通道要靠私有命令激活。光发
AT+CGACT=1,1(标准 PDP 激活)是不够的——ECM 数据口必须再收到AT+ZGACT=1,1才会真正通数据。这个命令只有它家 CPE 会用。 - 21 秒自复位。模块要求主机定时轮询(CSQ / CEREG 之类的保活),不轮询就自杀重启。初始化和心跳序列是作者把它家 CPE 的
RAtlog日志逆出来的:AT+ZEACT=2→AT+CGDCONT=1,IP,CMNET,,0,0→AT+ZGAAT=0→AT+CFUN=1→AT+ZSET=EXCEPT_RESET,1→AT+ZEMCI=0。 - 还有一堆标准命令它不认识。
ATZ、AT+GCAP、AT+WS46=?统统回+CME ERROR: 6003——而 ModemManager 初始化第一步就发ATZ,直接卡死在「启用模块」阶段。
方案演进:从守护进程到内核驱动
阶段一:用户态守护进程(能跑,但丑)
最初作者写了个 Python 守护进程:开机 → 打开 AT 口 → 发初始化序列 → 拨号 → 解析 +ZGIPDNS → 自己配 IP / 路由。能上网,问题是一堆:
- 要在 systemd 里装服务、装 udev 规则、装 modprobe 黑名单——换个发行版就全废,完全没有即插即用;
- ModemManager 只能看不能用(PPP 拨号被拒),任务栏里的「移动数据」永远连不上;
- 守护进程和 MM 抢 AT 口,得靠 udev 规则让 MM 装瞎。
能用,但作者很不爽:这明明应该是驱动的活。
阶段二:全部下沉进内核(最终方案)
最终方案是两个内核模块,用户态零辅助进程:
| 模块 | 职责 |
|---|---|
zte_ecm.ko |
基于 usbnet / usbnet_generic_cdc_bind,绑定 if1(ECM 数据口),生成标准网卡 enpXsYfZuVi1 |
zte_atfix.ko |
AT 命令修复 + 伪装 + 真实拨号翻译 + 内核态初始化 / 保活 + +ZGIPDNS 缓存 + %IPDPADDR 应答 |
驱动是怎么把 ModemManager 骗过去的
让 MM 直接管理这块模块的核心思路:把它伪装成 MM 已经完美支持的另一类模块。作者选了 ZTE 的 Icera 系列(比如 MF821)——因为 MM 的 ZTE 插件对「非 Icera」模块会直接忽略网卡口,而对 Icera 模块则会走「NET 口 + 静态 IP」的 bearer 路径,正好契合这台 ECM-only 的机器。
| MM 发出的命令 | 驱动的处理 |
|---|---|
ATZ |
重写为 ATE0 再转发 |
AT+GCAP |
伪造 +GCAP: +CGSM,+CLTE(让 MM 知道这是 LTE 模块,才会开 EPS 跟踪) |
AT+WS46=? / AT+WS46=<n> |
伪造模式列表 / 直接回 OK |
AT%IPSYS? |
伪造 %IPSYS: 0,1,0——关键中的关键,MM 的端口探测只要在响应里看到 %IPSYS: 就把模块归类为 Icera |
AT%IPDPACT=<cid>,1 |
不转发(模块不认识),驱动在私有口上真正执行 AT+CGACT=1,1 + AT+ZGACT=1,1,等数据通道起来后再回 OK 和一条 %IPDPACT: <cid>,1,0 URC |
AT%IPDPADDR=<cid> |
用嗅探到的 +ZGIPDNS 缓存构造标准响应:IP / 网关(自动 IP+1)/ DNS,让 MM 拿到一份「静态 IPv4 配置」 |
另外几个细节:
- 私有口隐身术。驱动接管了 if2(原本给守护进程用的口),做法是把 ModemManager 探口时发的裸
AT探测直接吞掉——MM 探不到响应,就把这个口标记为「非 AT 口」自动无视。驱动自己的命令走 bulk 端点,根本不过 tty 层。 - 内核心跳。probe 完成后自动跑 CPE 逆向出来的初始化序列,然后每 2 秒轮询一次
AT+CSQ/AT+CEREG?/AT+COPS?——21 秒自复位从此绝迹。 - autosuspend 也在驱动里关掉,不再需要 udev 规则。
- APN 自动识别。驱动读 IMSI 前五位,46000/46002/46004/46007/46008 → CMNET,46001/46006/46009 → 3gnet,46003/46005/46011 → ctnet。
实测效果
装上之后,ModemManager 的视角:
Status | state: registered
| signal quality: 100%
| operator name: CHINA MOBILE
| packet service state: attached
| ports: enp0s21f0u5i1 (net), ttyUSB0 (at)
| ignored ports: ttyUSB1 (unknown), ttyUSB2 (unknown)
NetworkManager 里直接出现「移动数据」,点一下就连上,网卡自动配好 IP / 默认路由 / DNS:
$ nmcli connection up "China Mobile"
连接已成功激活
$ ip addr show enp0s21f0u5i1
inet 10.x.x.x/32 scope global noprefixroute enp0s21f0u5i1
$ ping -I enp0s21f0u5i1 -c 3 223.5.5.5
3 packets transmitted, 3 received, 0% packet loss
rtt min/avg/max/mdev = 51.5/56.4/61.8/4.2 ms
$ curl --interface enp0s21f0u5i1 -o /dev/null -w '%{http_code}' http://www.baidu.com
200
重启电脑?模块自动加载,MM 自动认卡,NM 自动连接。全程无守护进程、无 udev 规则、无手工命令——这才是即插即用该有的样子。
⚠️ Tips
- mSATA 不能热插拔!一定要关机 → 插拔 → 开机。别问,问就是作者重启了三次。
- 别折腾 QMI。这模块的 QMI 是残废的,一探就崩。内核里建议把
qmi_wwan拉黑。 - IPv6 别指望。固件虽然声称支持 IPV6 PDP(实测能激活、能拿到链路本地地址和 DNS),但 ECM 通道没有任何 RA/NDP 转发能力,拿不到全局地址,实际不可用。
- 网络制式看硬件。TD-LTE 版本只在硬件上校准了 GSM / TD-SCDMA / LTE,插联通的卡是收不到信号的——这不是锁,是物理上就没有 WCDMA 校准数据,别去折腾什么解锁码和全网通固件。
- 不同批次的模块固件可能略有差异,本文基于
ZTE_MF253SV1.0.0B01。
后续:一个不得不做的妥协(ModemManager 与 Icera 插件)
文章发出之后有朋友来问:GNOME 里的网络模式为什么会蹦出个 5G?还有 2G/3G 都退网了,为什么还留着这些选项?作者去翻了一圈,发现这是 ModemManager 的历史包袱,值得展开讲讲。
先说背景:这块 ZTE 模块必须伪装成 ZTE Icera 机型,ModemManager 才会把它的 ECM 网口当作数据通道(非 Icera 的 ZTE 模块会被 MM 直接忽略网卡口,那数据就走不了了)。但 MM 的 Icera 插件写于 LTE 普及之前,它的模式/制式映射里根本没有 LTE——上游没有,作者机器上这个 1.25.95 定制版也没有(二进制反汇编确认过)。于是就有了下面这些”善意谎言”:
- 5G 是哪来的:MM 的 WS46 表里
25 = GERAN+UTRAN+E-UTRAN会被映射成MM_MODEM_MODE_ANY(全比特,含 5G),GNOME 就把 5G 列了出来。驱动现在改报+WS46: (28)(纯 E-UTRAN); - 模式选择器:Icera 类实际用
AT%IPSYS=?加载模式,而它的解析器只有 2G/3G(连 mode 4 = LTE 的分支都没有,日志会打unsupported mode found in %IPSYS=?: 4)。驱动伪造成 3G-only → GNOME 只显示一个”3G”条目,没有 5G、也没有 2G;用户在界面切换模式驱动也直接回 OK(模块实际仍锁在 LTE); - 网络类型显示:Icera 的
nwstate_to_act()同样没有 LTE 分支,驱动只能上报最接近的 HSPA+(GNOME 显示 3G)——技术串可以用nwstate_tech模块参数自己调; - 信号 100% 的锅:固件
+CSQ返回的是私有刻度253 + RSRP(比如 149 → RSRP -104 dBm,99/100 表示未知),MM 按标准 0-31 解析后直接钳位成满格。驱动把它归一化回标准刻度再上报,现在显示的是真实信号(约 60%)。
想要 GNOME 里显示真正的 4G/LTE?给 MM 的 Icera 插件打个小补丁就行:add_supported_mode() 加一个 case 4 → MM_MODEM_MODE_4G,nwstate_to_act() 加一个 "lte" 分支,一共十行左右。驱动层面到此为止——总不能强求一个 2013 年的插件认识 LTE 吧(笑)。
相关代码和说明已同步到仓库:github.com/xmb505/zte-mf253s
感谢
感谢恩山上那些拆机、抓日志、逆向固件的前辈们——没有你们掏出来的 RAtlog,就没有这篇博客;感谢 ModemManager 的源码(虽然全程都在研究怎么「骗」它,但读懂了是真的强);感谢 DeepSeek V4.1 Flash 帮作者啃完了 ModemManager 的插件逻辑和几万行 C 代码,还顺手把内核模块写完了。
折腾的意义就在于此:一块被运营商淘汰的电子垃圾,在 Linux 手里重新变成了好用的 4G 网卡。
—— DeepSeek V4.1 Flash 爱吃白饭的蓝色大肥鱼