由于内地各种互联网服务与手机强绑定的前提下,每个人手上的手机号码变得越来越多。在互联网上,早已有包括telegram-sms,SMS-forwarder等不同的应用被用来解决不想随身带着某张手机卡,却还需要拿他接收发送短信的场景。不过美中不足的是,由于这些应用均需要安装在手机上,这些短信转发应用均存在因国产android系统严格的后台限制被休眠导致无法转发短信的情况。同时,将带电池的旧手机长期插电也有一些安全隐患(电池鼓包等)。最重要的是,这些短信转发转发软件无法转移呼入和呼出的电话。为了解决上述的这些问题,在本文中,笔者基于EC20和东拼西凑的软件,实现了通过telegram等即时通讯软件收发短信,并通过SIP客户端从互联网呼出和接听电话。
笔者调研的其他方案
多卡宝
一到两年前非常流行的SIM卡托管方案,可以插4张卡,并且同时可待机两张卡,使用自有app转发短信及通话。根据FCCID的PDF,其使用了高通Snapdragon 210处理器,其常见于一些4G老人机上,鉴于此,笔者怀疑多卡宝使用了魔改的android系统。但在2021年下半年,多卡宝疑似因监管原因(可能是被用于电信诈骗?)被国内电商下架,并且因短信语音均需要经过三方服务器,也有一些安全隐患。
GOIP设备
俗称猫池,设备太贵,笔者负担不起。
材料及成本
EC20及周边设备
移远出品的一款4G卡,支持LTE Cat4,使用Snapdragon X5 LTE Modem,这个卡有很多个版本,有部分版本只带上网功能,不能接打电话和发短信。如果需要收发短信和打电话,请尽量购买最高级的EC20CEFAG-512-SGNS
,买mini-pcie接口的 ,移远的淘宝店买大概200一片,闲鱼购买大约50-60一片。
每张卡还需要另外加大约25元买一张4G模块转接板Mini PCIE转USB的卡座,卡座上有插SIM的地方(卡座一般用的是Mini Sim,如果只有nanosim的话可以去买一个卡套)。在淘宝上搜索「4G模块转接板Mini PCIE转USB」即可。
除此之外,还需购买若干根IPEX转SMA转接线及SMA接口的4G天线,淘宝上搜相应关键词即可。
电脑主机
要求不高,一般只要有usb口和有线网口即可,建议安装pve或esxi等虚拟机管理系统。(树莓派不一定行,因为供电不足)。笔者单独去闲鱼上买了一台Optiplex 3050mff,配i3-6100T,8G内存,256G SSD,大概花费了600元。
配置EC20
确认EC20能够正常读取SIM卡
关闭SIM卡的PIN,插入卡座,把EC20接上天线并通电,此时应该可以在/dev
里看到若干个ttyUSB端口:
ttyUSB0
ttyUSB1 PCM语音,GPS信号
ttyUSB2 控制命令
ttyUSB3
使用minicom打开ttyUSB2端口:
minicom -D /dev/ttyUSB2
# 输入ATI看一下EC20的版本号:
ATI
Quectel
EC20F
Revision: EC20CEFAGR06A15M4G
如果一切正常的话,可以先重置一遍EC20,以防上一个用户在卡内设置了错误的配置(但不要经常重置EC20,重置操作对dongle的闪存有损耗)。
重置模块 at+qprtpara=3
重启 AT+CFUN=1,1
重置并重启完后,可以通过以下命令检查一下SIM卡是否已经注册成功了(下面的例子是联通的,其他运营商同理):
AT+COPS?
+COPS: 0,0,"CHN-UNICOM",7
AT+QNWINFO
+QNWINFO: "FDD LTE","46001","LTE BAND 3",1650
AT+QENG="servingcell"
+QENG: "servingcell","CONNECT","LTE","FDD",460,01
配置VoLTE
随着三大运营商开始逐步退网2G及3G,为dongle配置VoLTE也变得十分必要了。以下流程参考了此PDF:
打开ims AT+QCFG="ims",1
查看dongle内的mbn文件 AT+QMBNCFG="List"
+QMBNCFG: "List",0,1,1,"ROW_Generic_3GPP",0x05010824,201806201
+QMBNCFG: "List",1,0,0,"OpenMkt-Commercial-CU",0x05011510,201911151
+QMBNCFG: "List",2,0,0,"OpenMkt-Commercial-CT",0x0501131C,201911141
+QMBNCFG: "List",3,0,0,"Volte_OpenMkt-Commercial-CMCC",0x05012011,201904261
# 尽管这里列出了移动联通电信的VoLTE配置文件,但使用默认的自动选择CU/CT/CMCC并不能注册VoLTE,在摸索很久之后,笔者发现需要强制选择ROW_Generic_3GPP才能成功注册VoLTE。
关闭自动选择mbn文件 AT+QMBNCFG="AutoSel",0
反激活当前的mbn at+qmbncfg="deactivate"
强制选择3gpp AT+QMBNCFG="select","ROW_Generic_3GPP"
重启 AT+CFUN=1,1
可以再确认一下mbn的选择状态,如果ROW_Generic_3GPP的第二位和第三位都是1的话,说明dongle目前选择了这个配置 AT+QMBNCFG="List"
+QMBNCFG: "List",0,1,1,"ROW_Generic_3GPP",0x05010824,201806201
+QMBNCFG: "List",1,0,0,"OpenMkt-Commercial-CU",0x05011510,201911151
+QMBNCFG: "List",2,0,0,"OpenMkt-Commercial-CT",0x0501131C,201911141
+QMBNCFG: "List",3,0,0,"Volte_OpenMkt-Commercial-CMCC",0x05012011,201904261
重启完后检查ims的状态 AT+QCFG="ims"
如果返回的是 +QCFG: "ims",1,1 即为激活,如果是+QCFG: "ims",1,0 说明没有激活
可选(激活UAC数字音频)
参考https://github.com/IchthysMaranatha/asterisk-chan-quectel/discussions/2:
AT+QCFG="usbcfg",0x2C7C,0x0125,1,1,1,1,1,0,1
这个命令同时也会打开一个adb daemon,可以通过adb shell进到模块自带的一个android系统里。如果发现系统有密码的话(有无密码均不影响后续配置),可以用adb pull把dongle的/etc/passwd
拉出来,去掉root的密码之后push回去。
激活之后可以通过aplay -L
查看有没有一个android设备。
dmix:CARD=Android,DEV=0
Android, USB Audio
Direct sample mixing device
安装asterisk虚拟机
freepbx自带的asterisk耦合了很多freepbx相关的配置,令笔者无从下手,同时centos似乎没有办法读取dongle的UAC接口,因此笔者参考quectel channel驱动作者的一篇讨论配置了一个简易的asterisk系统。笔者在这里使用的是Debian11,安装的是包管理器里自带的asterisk 16。安装asterisk之后记得把dongle的USB接口直通进虚拟机。
安装asterisk和一些依赖
apt update
apt install asterisk asterisk-dev adb git autoconf automake libsqlite3-dev build-essential libasound2-dev alsa-utils
下载asterisk-chan-quectel:
git clone https://github.com/IchthysMaranatha/asterisk-chan-quectel
cd asterisk-chan-quectel
./bootstrap
./configure --with-astversion=16
make
make install
随后把uac/quectel.conf
复制到/etc/asterisk
里。并通过systemctl restart asterisk
重启asterisk。
输入asterisk -rvvv
进入asterisk的cli界面并输入quectel show devices
即可看到识别到的dongle了,也能看到dongle的imei和SIM卡的imsi:
# asterisk -rvvv
Asterisk 16.16.1~dfsg-1+deb11u1, Copyright (C) 1999 - 2018, Digium, Inc. and others.
Created by Mark Spencer <[email protected]>
Asterisk comes with ABSOLUTELY NO WARRANTY; type 'core show warranty' for details.
This is free software, with components licensed under the GNU General Public
License version 2 and other licenses; you are welcome to redistribute it under
certain conditions. Type 'core show license' for details.
=========================================================================
Connected to Asterisk 16.16.1~dfsg-1+deb11u1 currently running on debian-asterisk (pid = 1403)
debian-asterisk*CLI> quectel show devices
ID Group State RSSI Mode Submode Provider Name Model Firmware IMEI IMSI Number
quectel0 0 Free 27 0 0 CHN-UNICOM EC20F EC20CEFAGR06A15M4 86XXXXX 46XXXXX Unknown
debian-asterisk*CLI>
配置dialplan
直接参考驱动作者写的帖子,下载帖子里的sipext.zip,解压后放到/etc/asterisk
下,同时修改一下/etc/asterisk/extensions.conf
(请不要直接照抄!根据自己的实际情况和驱动作者的帖子修改):
[incoming-mobile]
;exten => _.,1,Dial(SIP/70/100)
;same => n,Hangup()
exten => sms,1,Verbose(Incoming SMS from ${CALLERID(num)} ${BASE64_DECODE(${SMS_BASE64})})
;store
exten => sms,n,System(echo '${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)} - ${QUECTELNAME} - ${CALLERID(num)}: ${BASE64_DECODE(${SMS_BASE64})}' >> /var/log/asterisk/sms.txt)
;for tg bot use
exten => sms,n,System(echo '${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)} - ${QUECTELNAME} - ${CALLERID(num)}\n${BASE64_DECODE(${SMS_BASE64})}' >> /var/log/asterisk/unread_sms/${STRFTIME(${EPOCH},,%Y%m%d%H%M%S)}-${CALLERID(num)}.txt)
exten => sms,n,Hangup()
exten => ussd,1,Verbose(Incoming USSD: ${BASE64_DECODE(${USSD_BASE64})})
exten => ussd,n,System(echo '${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)} - ${QUECTELNAME}: ${BASE64_DECODE(${USSD_BASE64})}' >> /var/log/asterisk/ussd.txt)
exten => ussd,n,Hangup()
exten => _.,1,Dial(SIP/70/100)
exten => s,n,Hangup()
[Outbound-1001]
exten => _.,1,Dial(Quectel/quectel0/${EXTEN})
same => n,Hangup()
修改完后再重启一次asterisk。
测试收发短信
此时可以尝试发个短信给10010,测试一下收发短信:
发短信(给10010发cxll)
asterisk -rx 'quectel sms quectel0 10010 "cxll"'
收短信
根据前面的dialplan,收到短信后,asterisk会直接把短信内容写进/var/log/asterisk/sms.txt
,类似于这样:
tail -f /var/log/asterisk/sms.txt
2022-10-01 00:00:00 - quectel0 - 10010: 【权益领取提醒】尊敬的用户,您已获得2个月视频会员体验资格(原价15元/月),腾讯、爱奇艺、优酷、芒果TV、QQ音乐等20款会员每月任选1款,点击 https://u.10010.cn 即可参与(活动规则详见页面说明,限48小时内参与,短信转发无效,已办理请忽略)。如需屏蔽,请在“广东联通”官方公众号内回复“TD”,首次关注可领4GB流量【广东联通】
如果需要进一步转发短信,直接读取这个文件,或者修改dialplan中输出短信的格式即可。
用Telegram Bot自助收发短信
以下脚本都是用chatgpt写的,笔者用起来暂时没有遇到明显的问题:
发短信
Systemd service:
[Unit]
Description=Outbound SMS service for TG-SMS
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
ExecStart=/opt/tg-sms/outbound/.venv/bin/python3 /opt/tg-sms/outbound/outbound-sms.py
Restart=on-failure
RestartSec=5
User=asterisk
[Install]
WantedBy=multi-user.target
脚本(需要安装老版本的python-telegram-bot pip install python-telegram-bot==13.15
):
from telegram import Update, ForceReply
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, CallbackContext
# We'll use a dictionary to store each user's state.
user_data = {}
# List of allowed user/group IDs.
ALLOWED_IDS = [11111111]
def send(update: Update, context: CallbackContext) -> None:
if update.message.chat_id not in ALLOWED_IDS:
update.message.reply_text('您没有权限使用这个 bot。')
return
if len(context.args) != 1:
update.message.reply_text('请按照以下格式发送命令:/send <phone_number>')
return
user_data[update.message.chat_id] = {'phone_number': context.args[0]}
update.message.reply_text('请输入您要发送的信息:', reply_markup=ForceReply())
def cancel(update: Update, context: CallbackContext) -> None:
user_data.pop(update.message.chat_id, None)
update.message.reply_text('操作已取消。')
def handle_message(update: Update, context: CallbackContext) -> None:
if update.message.chat_id not in user_data:
return
if 'message' not in user_data[update.message.chat_id]:
user_data[update.message.chat_id]['message'] = update.message.text
update.message.reply_text('请确认信息:\n手机号:{}\n信息:{}'.format(user_data[update.message.chat_id]['phone_number'], user_data[update.message.chat_id]['message']), reply_markup=ForceReply())
else:
confirmation = update.message.text
if confirmation.lower() == 'yes':
import os
result = os.popen('asterisk -rx \'quectel sms quectel0 {} "{}"\''.format(user_data[update.message.chat_id]['phone_number'], user_data[update.message.chat_id]['message'])).read()
update.message.reply_text('命令执行结果:\n{}'.format(result))
else:
update.message.reply_text('操作已取消。')
user_data.pop(update.message.chat_id, None)
def main() -> None:
updater = Updater(token='your_token', use_context=True)
dispatcher = updater.dispatcher
dispatcher.add_handler(CommandHandler("send", send))
dispatcher.add_handler(CommandHandler("cancel", cancel))
dispatcher.add_handler(MessageHandler(Filters.text & ~Filters.command, handle_message))
updater.start_polling()
updater.idle()
if __name__ == '__main__':
main()
收短信
import requests
import sys
import urllib.parse
import re
# extract verify code
def extract_verification_code(sms_text):
# 定位到“验证码”关键词
keyword_index = sms_text.find('验证码')
if keyword_index == -1:
return None
# 从关键词后开始匹配4-6位的数字
match = re.search(r'(\d{4,6})', sms_text[keyword_index:])
if match:
return match.group(1)
return None
# tg
tg_bot_token = ""
tg_send_msg_url = "https://api.telegram.org/bot"+tg_bot_token+"/sendMessage"
tg_receive_msg_url = "https://api.telegram.org/bot"+tg_bot_token+"/getUpdates"
# wecom
wecom_send_msg_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-key"
bark_send_msg_url = "https://api.day.app/bark-token/"
def incoming_sms_tg(msg_text):
send_body = {}
send_body['chat_id'] = '11111111'
send_body['text'] = msg_text
requests.post(tg_send_msg_url, json=send_body)
def incoming_sms_wecom(msg_text):
send_body = {}
send_body['msgtype'] = 'text'
send_body['text'] = {}
send_body['text']['content'] = msg_text
requests.post(wecom_send_msg_url, json=send_body)
def incoming_sms_bark(msg_text):
requests.get(bark_send_msg_url + urllib.parse.quote_plus(msg_text) +
"?copy=" + extract_verification_code(msg_text))
def incoming():
msg_text = sys.argv[1]
incoming_sms_wecom(msg_text)
incoming_sms_bark(msg_text)
incoming_sms_tg(msg_text)
incoming()
安装freepbx虚拟机
去freepbx官网上下载freepbx的iso镜像(看起来是一个CentOS7):https://www.freepbx.org/downloads/。使用镜像安装系统,安装时选择freepbx 16 with asterisk 18
。安装完后用浏览器访问虚拟机的IP,设置初始的管理员密码(最开始可以暂不打开防火墙,方便配置)。
添加分机号
在 Applications-Extensions 里,点击add extension- SIP extension,加一个200的extension(号码随意,只要不和asterisk虚拟机里的号码撞上了就行):
剩下部分保持默认,点submit,并点击一下右上角的apply config。
添加Trunk
添加之前,先按照前面的帖子的指引,修改asterisk虚拟机里的/etc/asterisk/sip.conf
,把最底下70分机的host=192.168.x.x
改成freepbx虚拟机的IP,重启asterisk。
在freepbx的Connectivity-Trunks里添加一个SIP Trunk,配置如下,其他默认:
路由
在Connectivity-Outbound Routes里,新建路由,将出方向的路由都转发给前一步创建的SIP trunk:
在Connectivity-Inbound Routes里,新建路由,将入方向的路由都转发给extensions-上面设置的分机号:
如果未来连接了多个分机或者多个dongle,需要根据用户进行分流的话,可以详细配置上面的DID和CallerID来进行过滤。
测试通话
下载一个免费版的zoiper,添加账户的时候用户名输入分机号@freepbx的IP
,密码即上面设置的密码(注意不要输错了,freepbx默认有打开fail2ban,输错SIP密码也会触发fail2ban,还需要手动去删除iptables规则)。
确认注册上了之后可以尝试通过zoiper呼出到10010或者是自己的电话,测试一下语音和按键的DTMF音有被识别到。如果是外部呼入dongle里的号码的电话,呼入到freepbx之后会被直接转移给分机,此时zeoiper会有提示,直接点接听即可。
端口转发,SIP push等
对于sip,除了需要转发sip的默认端口(5201,可以在freepbx的设置里查看(settings - asterisk sip setting
),还需要转发RTP端口。如果只想转发一个端口,可以考虑参考EC20 模块+Issabel 实现网络电话 - Hanako Blog使用IAX2 extension。SIP Push(在接到电话时在手机上弹推送)目前只有zoiper提供了比较方便的配置方式,但需收费,具体可参考 如何把SIM卡从手机中取出 – IAM-LC。
使用模块上网
如果使用的手机卡包含流量,我们也可以一并配置模块的上网功能,以便在机器的有线/无线网络挂掉后,还能正常的转发短信和通话。
首先,在连上串口后输入AT+QCFG="usbnet",1
,设置usbnet模式为ECM(1是ECM,2是NDIS,3是RNDIS)
接着,用AT+CFUN=1,1
重启一下模块,随后查看ip a
,应该能看到一个类似enxe2eeabcd123
的接口,在这个接口上直接运行dhclient即可获取v4或v6地址。如果怕interface的名字经常变化,可以参考这个问题,把interface的名字根据MAC地址重命名成quectel-usbX
这样方便管理的名字。
3: quectel-usb0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 1000
link/ether ff:ff:ff:ff:ff:ff brd ff:ff:ff:ff:ff:ff
inet 192.168.225.41/24 brd 192.168.225.255 scope global dynamic quectel-usb0
valid_lft 28802sec preferred_lft 28802sec
inet6 2408::/64 scope global dynamic mngtmpaddr
valid_lft forever preferred_lft forever
inet6 fe80::/64 scope link
valid_lft forever preferred_lft forever
其他
- 在不打开SIP客户端时,打到dongle上的电话会提示用户忙,需要使用前一节中提到的sip push的方式,或者保持客户端常开才能正常接到电话。
参考资料
- EC20的AT指令集可以在https://www.quectel.com/ProductDownload/EC20.html处下载。
- asterisk上的Quectel驱动: https://github.com/IchthysMaranatha/asterisk-chan-quectel/
- https://gao.md/blog/2014/10/05/sms-gateway-setup-huawei-e1750-asterisk-chan-dongle/
- https://iam.lc/2024/01/how-to-get-rid-of-sim.ping
- https://forums.quectel.com/t/ec25-e-mini-additional-mbn-files/13473/2