由于内地各种互联网服务与手机强绑定的前提下,每个人手上的手机号码变得越来越多。在互联网上,早已有包括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端口:
1 2 3 4 ttyUSB0 ttyUSB1 PCM语音,GPS信号 ttyUSB2 控制命令 ttyUSB3
使用minicom打开ttyUSB2端口:
1 2 3 4 5 6 7 8 minicom -D /dev/ttyUSB2 # 输入ATI看一下EC20的版本号: ATI Quectel EC20F Revision: EC20CEFAGR06A15M4G
如果一切正常的话,可以先重置一遍EC20,以防上一个用户在卡内设置了错误的配置(但不要经常重置EC20,重置操作对dongle的闪存有损耗)。
1 2 重置模块 at+qprtpara=3 重启 AT+CFUN=1,1
重置并重启完后,可以通过以下命令检查一下SIM卡是否已经注册成功了(下面的例子是联通的,其他运营商同理):
1 2 3 4 5 6 7 8 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 :
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 打开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 :
1 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设备。
1 2 3 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和一些依赖 1 2 apt update apt install asterisk asterisk-dev adb git autoconf automake libsqlite3-dev build-essential libasound2-dev alsa-utils
1 2 3 4 5 6 7 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 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
(请不要直接照抄!根据自己的实际情况和驱动作者的帖子 修改):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [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) 1 asterisk -rx 'quectel sms quectel0 10010 "cxll"'
收短信 根据前面的dialplan,收到短信后,asterisk会直接把短信内容写进/var/log/asterisk/sms.txt
,类似于这样:
1 2 3 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收发短信的python脚本(请自行修改token和chat id。脚本未经过完整测试,可能会有没有充分考虑的case,请谨慎使用):
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 57 58 59 60 61 62 63 import osimport requestsimport jsonimport timeimport scheduletg_bot_token = "TOKEN" sms_log_dir = "/var/log/asterisk/unread_sms/" 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" tg_receive_msg_init_count = 0 def receive_sms (): print ("recv poll start" ) for file in os.listdir(sms_log_dir): f = open (sms_log_dir+file, "r" ) sms_text = f.read() send_body = {} send_body['chat_id' ] = "CHAT ID" send_body['text' ] = sms_text requests.post(tg_send_msg_url, json=send_body) os.remove(sms_log_dir+file) def update_tg_receive_msg_init_count (): new_msg = requests.get(tg_receive_msg_url).json() if new_msg['result' ]: with open ('processed_update_id.txt' , 'w' ) as f: f.write(str (new_msg['result' ][-1 ]['update_id' ])) def send_sms (): print ("send poll start" ) f = open ('processed_update_id.txt' , "r" ) processed_update_id = f.read() new_msg = requests.get(tg_receive_msg_url).json() if new_msg['result' ]: for i in new_msg['result' ]: if i['update_id' ] > int (processed_update_id): with open ('processed_update_id.txt' , 'w' ) as g: g.write(str (i['update_id' ])) if ('text' in i['message' ]): command = str (i['message' ]['text' ]) if command.startswith('/sendsms' ): command_list = command.split(' ' ) if len (command_list) == 4 : dongle = command_list[1 ] dest = command_list[2 ] context = command_list[3 ] asterisk_command = "asterisk -rx " + "'quectel sms " + dongle + " " + dest + " " + context + "'" print (asterisk_command) os.system(asterisk_command) schedule.every(5 ).seconds.do(send_sms) schedule.every(5 ).seconds.do(receive_sms) update_tg_receive_msg_init_count() while True : schedule.run_pending() time.sleep(1 )
安装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客户端时,打到dongle上的电话会提示用户忙,理论上我们可以在freepbx上设置等待时间,并通过dialplan让dongle在接打电话时调用System触发脚本通知用户,用户打开SIP客户端即可接听电话。 参考资料