Featured image of post Asterisk + EC20 实现短信收发+语音通话+网络代理

Asterisk + EC20 实现短信收发+语音通话+网络代理

无需 FreePBX,在 Ubuntu 24 实现短信转发和来电通知

起因

笔者有一张新加坡 eight 的电话卡,带有一些中国大陆漫游流量,也支持在国内语音漫游和接收短信。之前一直使用备用 Android 机 + SmsForwarder 来接收短信通知,体验还是不错的。不过,想要远程接打电话不太可能,并且想利用这张卡的漫游流量也不太容易。于是,笔者决定研究一下 4G 模块的方案。

购买 Quectel EC20 模块

笔者从闲鱼购买了 EC20CEFAG-512-SGNS 模块 + USB 转接板 + 天线,总共花费 60 左右,还是很合算的。美中不足的是配套的天线太弱,导致网速很慢,笔者后来又新购了较长的天线。购买时需要注意,EC20 有多个版本,因为我们希望支持通话和短信,建议直接购买全功能的 EC20CEFAG 版本。另外,购买时最好让商家检查一下模块固件版本,确保是 EC20CEFAGR06AXXM4G 或者 EC20CEFAGR08AXXM4G,具体原因下面会说明。

更新固件

刚刚到手时,EC20 模块的固件版本是 EC20CEFAGR06A10M4G。笔者测试了 eight 电话卡,功能正常,但笔者的中国电信卡却无法进行短信收发和语音通话。经过一番查找,发现 R06 基线的固件较老,对新版中国电信卡的支持不好,需要升级到新的 R08 基线固件。

EC20 的固件获取比较麻烦,因为移远没有把固件放在官网上。许多人是通过在论坛发帖的方式获得固件的,不过论坛上的技术支持回复往往较慢。笔者由于在淘宝上的移远旗舰店购买过一些东西,所以直接联系客服获取了最新的 R08 固件。另外闲鱼上也有一些卖家出售固件。截止笔者写作时(2026/01/20),最新的 EC20 固件版本是 EC20CEFAGR08A03M4G

获取固件之后,从官网下载 Quectel QFlash 工具,按照 PDF 里的说明更新固件即可。

配置模块

笔者使用的系统为 Ubuntu 24。下面的测试均使用中国电信卡进行。

连接到串口

首先在 Ubuntu 上安装 minicom

1
sudo apt install minicom

然后插入模块,检查 /dev/ 下新增的设备,会出现四个 /dev/ttyUSB* 设备:

1
/dev/ttyUSB0  /dev/ttyUSB1  /dev/ttyUSB2  /dev/ttyUSB3

其中,/dev/ttyUSB2 是 AT 命令端口,使用 minicom 连接:

1
sudo minicom -D /dev/ttyUSB2

重置模块

输入 AT+QPRTPARA=3 重置模块,避免之前的配置影响测试。之后输入 AT+CFUN=1,1 重启模块。

AT 命令测试

首先查看下模块信息。输入 ATI

1
2
3
4
5
Quectel
EC20F
Revision: EC20CEFAGR08A03M4G

OK

固件版本已经是最新的 R08 基线。接下来输入 AT+COPS?,查看注册状态:

1
2
3
+COPS: 0,0,"CHN-CT",7

OK

可以看到,已经成功注册到中国电信网络了。

配置 VoLTE(可选)

VoLTE 有助于提高语音通话质量,并且类似 eight 这样的国外卡在国内必须使用 VoLTE 才能进行语音通话。输入 AT+QCFG="ims",1 启用 VoLTE。

之后,使用 AT+QCFG="ims" 检查当前 VoLTE 状态,如果显示 "ims",1,1 则表示成功激活。

配置 UAC 数字音频(可选)

UAC 数字音频也有助于提高通话质量。输入 AT+QCFG="usbcfg",0x2C7C,0x0125,1,1,1,1,1,0,1。之后在 Ubuntu 终端运行 aplay -L,可以看到新增了音频设备:

1
2
3
hw:CARD=Android,DEV=0
    Android, USB Audio
    Direct hardware device without any conversions

配置网络

为了方便之后配置代理,现在可以修改好模块的网络模式。输入 AT+QCFG="usbnet",1,将 USB 网络模式设置为 ECM 模式。之后输入 AT+CFUN=1,1 重启模块。重启后,Ubuntu 会自动识别出一个新的网络接口 enx...

退出 minicom

按下 Ctrl-A,然后按 X,选择 Yes 退出 minicom。

安装 Asterisk

笔者此处和其他教程的区别是没有使用 FreePBX,而是直接使用 Asterisk 进行配置。可以将 FreePBX 看作是 Asterisk 的一个图形化管理界面,由于我们的使用场景较为简单,可以直接手动配置 Asterisk,免去了安装 FreePBX 的麻烦。

安装 Asterisk 和依赖

1
sudo apt install asterisk asterisk-dev adb git autoconf automake libsqlite3-dev build-essential libasound2-dev alsa-utils

编译安装 asterisk-chan-quectel 模块

1
2
git clone https://github.com/IchthysMaranatha/asterisk-chan-quectel
cd asterisk-chan-quectel

笔者这里按照另一篇文章的说明,修改了 pdu.c 文件中 663 行左右的代码:

1
2
3
int i = 0;
int sca_digits = (pdu[i++] - 1) * 2;
int field_len = pdu_parse_number(pdu + i, pdu_length - i, sca_digits, sca, sca_len);

修改为:

1
2
3
4
5
6
int i = 0;
int sca_digits = (pdu[i++] - 1) * 2;
if (pdu[i-1] == 0) {
    return i;
}
int field_len = pdu_parse_number(pdu + i, pdu_length - i, sca_digits, sca, sca_len);

之后运行 asterisk -V,查看 Asterisk 版本号:

1
Asterisk 20.6.0~dfsg+~cs6.13.40431414-2build5

然后以下命令,编译安装 asterisk-chan-quectel 模块:

1
2
3
4
./bootstrap
./configure DESTDIR=/usr/lib/x86_64-linux-gnu/asterisk/modules --with-astversion=20.6.0
make
sudo make install

之后,将 quectel.conf 复制到 /etc/asterisk/ 目录下。如果你之前激活了 UAC 数字音频,将配置文件末尾部分中的两行取消注释:

1
2
3
4
5
[quectel0]
audio=/dev/ttyUSB1		         ; tty port for Audio, set as ttyUSB4 for Simcom if no other dev present
data=/dev/ttyUSB2		         ; tty port for AT commands; 		no default value
quec_uac=1                              ; Uncomment line if using UAC mode
alsadev=hw:CARD=Android,DEV=0           ; Uncomment if using UAC, set device name or index as reqd

最后,运行 sudo systemctl restart asterisk 重启 Asterisk 服务。

设置权限

Ubuntu 安装的 Asterisk 默认使用 asterisk 用户运行,该用户无权访问 /dev/ttyUSB 设备。需要将该用户添加到 dialout 组:

1
sudo usermod -aG dialout asterisk

另外,Ubuntu 上默认安装的 modemmanager 也会干扰 EC20 模块的使用,将其禁用:

1
2
sudo systemctl stop ModemManager
sudo systemctl disable ModemManager

之后重启系统。

检查 EC20 模块状态

重启后,运行 sudo asterisk -rvvv 进入 Asterisk CLI,输入 quectel show device state quectel0 检查模块状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
-------------- Status -------------
  Device                  : quectel0
  State                   : Free
  Audio                   : /dev/ttyUSB1
  Data                    : /dev/ttyUSB2
  Voice                   : Yes
  SMS                     : Yes
  Manufacturer            : Quectel
  Model                   : EC20F
  Firmware                : EC20CEFAGR08A03M4G
  IMEI                    : XXXXXX
  IMSI                    : XXXXXX
  GSM Registration Status : Registered, home network
  RSSI                    : 27, -59 dBm
  Mode                    : No Service
  Submode                 : No service
  Provider Name           : CHN-CT
  Location area code      : XXXXXX
  Cell ID                 : XXXXXX
  Subscriber Number       : Unknown
  SMS Service Center      : XXXXXX
  Use UCS-2 encoding      : Yes
  Tasks in queue          : 0
  Commands in queue       : 0
  Call Waiting            : Disabled
  Current device state    : start
  Desired device state    : start
  When change state       : now
  Calls/Channels          : 0
    Active                : 0
    Held                  : 0
    Dialing               : 0
    Alerting              : 0
    Incoming              : 0
    Waiting               : 0
    Releasing             : 0
    Initializing          : 0

以上内容说明已经成功注册到中国电信网络,并且 Asterisk 能够正常访问 EC20 模块。由于是电信卡,我们可以向 10000 发送一条免费短信进行测试:

1
quectel sms quectel0 10000 "cxll"

看到 Successfully sent SMS message 即表示发送成功。如果不出意外,几秒钟后可以在终端里看到运营商的回复短信。

配置短信转发和语音通话

配置 SIP 账号

首先编辑 /etc/asterisk/pjsip.conf,在文件末尾添加以下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0

[1001]
type=endpoint
context=from-internal
disallow=all
allow=ulaw,alaw,g722,gsm
auth=auth1001
aors=1001
rewrite_contact=yes
rtp_symmetric=yes

[auth1001]
type=auth
auth_type=userpass
username=1001
password=YourStrongPassword ; 请修改为强密码

[1001]
type=aor
max_contacts=10

这里我们创建了一个 SIP 账号,后续我们将把 EC20 模块的来电转发到该账号,并将该账号拨出的电话通过 EC20 模块拨出。

禁用 chan_sip 模块

由于我们使用了 chan_pjsip,需要禁用默认的 chan_sip 模块,否则会产生冲突。编辑 /etc/asterisk/modules.conf

1
2
3
4
5
[modules]
autoload=yes

; Do not load the chan_sip since we are using chan_pjsip
noload => chan_sip.so

配置 extensions_custom.conf

现在我们来配置短信转发和语音通话。新建 /etc/asterisk/extensions_custom.conf,添加以下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
[from-internal]
exten => _[+0-9].,1,NoOp(Calling out via EC20: ${EXTEN})
same => n,Dial(Quectel/quectel0/${EXTEN})
same => n,Hangup()

[incoming-mobile]
; --- 短信处理 ---
exten => sms,1,Verbose(Incoming SMS from ${CALLERID(num)})
; 运行短信通知脚本
same => n,System(/usr/bin/python3 /etc/asterisk/scripts/sms_notify.py "${CALLERID(num)}" "${SMS_BASE64}" &)
; 保存短信内容到本地
same => n,System(echo '${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)} - ${QUECTELNAME} - ${CALLERID(num)}: ${BASE64_DECODE(${SMS_BASE64})}' >> /var/log/asterisk/sms.txt)
same => n,Hangup()

; --- USSD 处理 ---
exten => ussd,1,Verbose(Incoming USSD: ${BASE64_DECODE(${USSD_BASE64})})
same => n,System(echo '${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)} - ${QUECTELNAME}: ${BASE64_DECODE(${USSD_BASE64})}' >> /var/log/asterisk/ussd.txt)
same => n,Hangup()

; --- 语音来电处理 ---
exten => s,1,NoOp(Incoming call from ${CALLERID(num)})
same => n,Set(CALLERID(all)="${CALLERID(num)}" <${CALLERID(num)}>)
; 运行来电通知脚本
same => n,System(/usr/bin/python3 /etc/asterisk/scripts/call_notify.py "${CALLERID(num)}" &)
; 保存来电记录到本地
same => n,System(echo '${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)} - ${QUECTELNAME} - Incoming call from ${CALLERID(num)}' >> /var/log/asterisk/calls.txt)

; 设置循环参数
same => n,Set(MAX_RETRIES=8) ; 每 5 秒检查一次,共等待 40 秒
same => n,Set(COUNTER=0)

; 循环检查点
same => n(check_reg),NoOp(Checking if 1001 is online... Attempt ${COUNTER})
; 检查 PJSIP 1001 是否上线
same => n,Set(CONTACTS=${PJSIP_DIAL_CONTACTS(1001)})

; 如果有地址,跳转到拨号
same => n,GotoIf($[ "${CONTACTS}" != "" ]?dial_now)

; 如果无地址,判断是否超时
same => n,Set(COUNTER=$[${COUNTER} + 1])
same => n,GotoIf($[${COUNTER} >= ${MAX_RETRIES}]?timeout)

; 未超时则等待 5 秒重试
same => n,Ringing() ; 向主叫方播放回铃音
same => n,Wait(5)
same => n,Goto(check_reg)

; 拨号分支
same => n(dial_now),NoOp(1001 is online, dialing...)
same => n,Dial(PJSIP/1001,30)
same => n,Hangup()

; 超时分支
same => n(timeout),NoOp(1001 did not register in time. Hanging up.)
same => n,Hangup()

这里我们创建了两个拨号计划:from-internal 用于处理从 SIP 账号拨出的电话,incoming-mobile 用于处理来自 EC20 模块的短信和来电。相比与其他教程中的配置,该配置有如下特点:

  • 支持拨国际号码(以 + 开头的号码)
  • 短信、UUSD 和来电会被储存到本地文件中。同时,在收到短信和来电时,会调用 /etc/asterisk/scripts 中的外部 Python 脚本进行通知。如果你不需要脚本功能,可以将相关行删除
  • 一般来说,SIP 客户端需要保持在线,才能接听到来电。但这要求手机上的客户端一直与服务端连接,一是耗电,二是容易被手机系统杀进程,很不稳定。如果客户端不在线,按照其他教程中的设置,来电会被直接挂断。这里我们设置了一个循环检查机制,来电时如果客户端不在线,则会每 5 秒检查一次 SIP 客户端是否在线,最多等待 40 秒。由于来电时 Python 脚本将通知发送到微信,用户看到通知后会立即去打开 SIP 客户端。如果在 40 秒内客户端上线,则来电可以接通;如果超时,则 Asterisk 会挂断来电

Python 脚本可以按照如下框架编写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# sms_notify.py

import sys
import requests
import base64

caller_id = sys.argv[1]
msg_base64 = sys.argv[2]

def send_notification(cid, b64_content):
    try:
        content = base64.b64decode(b64_content).decode('utf-8')
    except:
        content = "Decode Error"

    # ...

if __name__ == "__main__":
    send_notification(caller_id, msg_base64)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# call_notify.py

import sys
import requests

caller_id = sys.argv[1]

def send_notification(cid):
    # ...

if __name__ == "__main__":
    send_notification(caller_id)

配置完 extensions_custom.conf 后,需要在 /etc/asterisk/extensions.conf 中包含该文件。在 /etc/asterisk/extensions.conf 末尾添加:

1
#include extensions_custom.conf

最后,重启 Asterisk 服务:

1
sudo systemctl restart asterisk

SIP 客户端设置

笔者使用 Groundwire 作为 Android 系统上的 SIP 客户端。

配置时,新建 SIP 账号,用户名填写 1001,密码填写之前在 pjsip.conf 中设置的密码,域名填写 Asterisk 所在机器的 IP 地址或者域名。

配置完成后,可以拨打 10000 进行测试。也可以顺便测试一下短信接收的通知脚本是否工作正常。

如果你想测试接听电话,可以先完全关闭 Groundwire,然后用另一个手机拨打 EC20 模块的号码。此时主叫手机会听到 5 秒一次的回铃音。重新打开 Groundwire,等待片刻,即可收到来电,接听后双方可以正常通话。

网络代理

固定网卡名称

由于 EC20 并没有内置固定 MAC 地址,导致每次重新插入后,Ubuntu 会按照随机生成的 MAC 地址创建不同的网卡名称,给后续配置带来麻烦。我们可以通过 udev 规则固定网卡名称。

首先查看 EC20 的 idVendoridProduct。运行 sudo lsusb

1
Bus 001 Device 005: ID 2c7c:0125 Quectel Wireless Solutions Co., Ltd. EC25 LTE modem

创建 /etc/udev/rules.d/70-ec20-net.rules 文件,添加以下内容:

1
SUBSYSTEM=="net", ACTION=="add", ATTRS{idVendor}=="2c7c", ATTRS{idProduct}=="0125", NAME="ec20"

运行 sudo udevadm control --reload 重载规则。重新插入 EC20 模块后,运行 ip a,可以看到网卡名称已经变为 ec20

配置静态地址、路由和度量值

EC20 模块的网络接口默认使用 DHCP 获取地址。为了方便配置,我们可以为其设置静态 IP 地址。编辑 /etc/netplan/01-network-manager-all.yaml,添加以下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Let NetworkManager manage all devices on this system
network:
  version: 2
  renderer: NetworkManager
  ethernets:
    ec20:
      dhcp4: false
      addresses: [192.168.225.2/24]
      routes:
        - to: 0.0.0.0/0
          via: 192.168.225.1
          metric: 999
      nameservers:
        addresses: [192.168.225.1]

我们在这里设置一个较大的路由度量值,确保默认路由不通过 EC20 模块。保存后运行 sudo netplan apply 应用配置。

搭建 SOCKS5 代理服务器

理想情况下,应该通过 Linux 网络命名空间来隔离 EC20 的网络流量,不过配置较为复杂。于是笔者使用 Go 语言编写了一个简单的 SOCKS5 代理服务器,将流量绑定在 EC20 网卡,并使用 192.168.225.1 作为 DNS 服务器。

项目地址:Mythologyli/go-socks5-server。读者可以在仓库中下载编译好的二进制文件,或者自行编译。

之后,运行 sudo ./go-socks5-server -bind=":10800" -dns="192.168.225.1:53" -iface=ec20,即可启动 SOCKS5 代理服务器。

使用 curl --socks5 127.0.0.1:10800 http://ip-api.com 测试代理是否工作正常。笔者这里使用了 eight 电话卡:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
  "status"       : "success",
  "continent"    : "Asia",
  "continentCode": "AS",
  "country"      : "Singapore",
  "countryCode"  : "SG",
  "region"       : "02",
  "regionName"   : "North East",
  "city"         : "Singapore",
  "district"     : "Ang Mo Kio",
  "zip"          : "560629",
  "lat"          : 1.38028,
  "lon"          : 103.84,
  "timezone"     : "Asia/Singapore",
  "offset"       : 28800,
  "currency"     : "SGD",
  "isp"          : "STARHUB-MOBILE",
  "org"          : "",
  "as"           : "AS4657 StarHub Ltd",
  "asname"       : "STARHUB-INTERNET",
  "mobile"       : true,
  "proxy"        : false,
  "hosting"      : false,
  "query"        : "117.20.x.x"
}

参考资料及致谢

本文大量参考了以下两篇文章:

以及,感谢 Google Gemini 的帮助。

使用 Hugo 构建
主题 StackJimmy 设计