近年来,手机与个人私隐形成的强绑定让人诟病。上至健康码,下至一个普通App,都可以通过手机号将一个人的身份同千丝万缕联系起来。研究如何通过解绑手机与人的直接关联,显得尤为重要。
手机运营商提供给用户的最主要的业务我认为有三个:语音、短信和数据。这里我们着重考虑前两种,即语音和短信业务的转发。
至于数据业务:“反正也不干不净,不要也罢。”
短信转发方案
不考虑语音转发的情况下,如果只需要做短信转发,方案可选性会更多而且更为简单。在考虑加入语音转发之前,我一直使用的是开源方案 Sms-Forwarder,使用一台闲置安卓手机做短信转发,可靠且稳定。
如果不想使用安卓手机,下文也提供基于PICe模块的方案,因和语音深度集成,放在一起讲。
语音+短信转发方案
最初的想法我其实是想提供USB上网卡的方案把SIM卡暴露给系统,再在系统层面进行一些操作。经过一番摸索,我发现现在能用的USB上网卡几乎已经没有了,即使有,USB也仅仅用作取电,不会过多交互数据。更要命的是大部分USB上网卡都是3G卡,随着3G退网这个方案想必无法持久使用。
经过一番摸索我确定用移远的方案,这是一个硬件提供商,提供各种成熟的LTE模块。而底层硬件我在这里用的是光影猫,无它,只因为闲置在工具箱里吃灰,而且正好带了一块LTE模块(购买原意是做4G软路由使用)。
部署完成回过头来看,涉及到的硬件如下:
- 移远EM05模块(EM05CEFC-128-SGAS)
- 光影猫 (安装 Debian 并安装 Asterisk)
- 一台 VM 安装 FreePBX
- 一台公网主机安装 FRP
- 一台前置机用于转发FRP流量
- 两台手机,用于调试语音业务
其中,EM05 安装于光影猫中,前置机、FreePBX 都从 homelab 中分配资源,公网主机也拿现成的直接用即可。但如果自己家里没有对应的基础设施配置,运行这个方案成本或许会比较高。
如果不想用光影猫的方案,通过购置4G模块转接板的MiniPCIe转USB卡座,将模块映射到虚拟机中,并在虚拟机中安装Debian,理论上也可以实现一样的作用。
配置移远EM05模块
首先我将光影猫刷为Debian系统并配置静态IP,接入内网,过程不表。值得一提的是机器不带时钟电池,如果没有成功同步时间可能会影响到apt和wget等操作。可以通过 date 命令检查时间并修正。
以下配置过程极大程度地借鉴了这篇文章,如果有不清楚的地方建议回原文查看。
进入系统后应可以识别到移远模块:
root@Photonicat /opt
$ ll /dev/ttyUSB
ttyUSB0 ttyUSB1 ttyUSB2 ttyUSB3
安装 minicom:
apt install minicom
通过以下命令打开与EM05的交互终端:
minicom -D /dev/ttyUSB2
查看版本号:
ATI
重置并重启:
重置模块 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:
AT+QCFG="ims",1
AT+QMBNCFG="List"
AT+QMBNCFG="AutoSel",0
at+qmbncfg="deactivate"
AT+QMBNCFG="select","ROW_Generic_3GPP"
重启 AT+CFUN=1,1
重启后确认状态:
AT+QCFG="ims"
如果返回的是 +QCFG: "ims",1,1 即为激活,如果是+QCFG: "ims",1,0 说明没有激活
AT+QMBNCFG="List"
如果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
配置Asterisk和短信转发
安装Asterisk和依赖:
apt update
apt install asterisk asterisk-dev adb git autoconf automake libsqlite3-dev build-essential libasound2-dev alsa-utils
通过移远提供的项目,可以使得Asterisk可以读取到移远模块。
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:
$ asterisk -rvvv
Asterisk 16.28.0~dfsg-0+deb10u4, Copyright (C) 1999 - 2021, Sangoma Technologies Corporation 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.28.0~dfsg-0+deb10u4 currently running on Photonicat (pid = 1545)
Photonicat*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 EM05 EM05CEFCR06A04M1G 867144039000000 460018660000000 Unknown
Photonicat*CLI>
配置 Dialplan
直接照抄没啥问题:
$ cat /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)
exten => sms,n,System(echo '${BASE64_DECODE(${SMS_BASE64})} - ${CALLERID(num)} - ${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)}' >> /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,Set(CALLERID(name)="")
exten => _.,1,Dial(SIP/70/100)
exten => s,n,Hangup()
[Outbound-1001]
exten => _.,1,Dial(Quectel/quectel0/${EXTEN})
same => n,Hangup()
炫一段GPT的解释:
这个Asterisk的dialplan包括两个上下文:[incoming-mobile]和[Outbound-1001],实现了以下功能: [incoming-mobile] 上下文 处理呼入短信: 当收到短信时,该上下文会捕捉到sms扩展。 第一个优先级(1)使用Verbose应用程序将来自特定电话号码的短信内容解码并记录在日志中。 第二个优先级(n)使用System应用程序将短信内容、发件人电话号码以及当前时间追加到/var/log/asterisk/sms.txt文件中。 第三个优先级(n)挂断通话。 处理呼入USSD: 当收到USSD消息时,该上下文会捕捉到ussd扩展。 第一个优先级(1)使用Verbose应用程序将USSD消息内容解码并记录在日志中。 第二个优先级(n)使用System应用程序将USSD消息内容和发送时间追加到/var/log/asterisk/ussd.txt文件中。 第三个优先级(n)挂断通话。 处理其他呼入事件: 对于任何其他事件,该上下文捕捉到特殊的_扩展名。 第一个优先级(1)使用Dial应用程序从SIP设备70拨打到SIP设备100。 第二个优先级(s)挂断通话。 [Outbound-1001] 上下文 处理外拨电话: 该上下文捕捉到特殊的_扩展名。 第一个优先级(1)使用Dial应用程序从Quectel设备quectel0拨打到指定的电话号码。 第二个优先级(n)挂断通话。
建议重启服务器而不是重启Asterisk,确定没有问题后测试短信:
asterisk -rx 'quectel sms quectel0 10010 "cxll"'
确定 /var/log/asterisk/sms.txt 可以收到即可。
配置短信转发
这里我用现成的shell脚本,丢给GPT直接做成一个python脚本,调用企业微信应用接口来发送短信。
脚本配置好ID、secret后摆到以下文件: /opt/send_notification.py
#!/usr/bin/env python3
import requests
import json
from pathlib import Path
import time
def get_json_value(json_obj, key, default):
return json_obj.get(key, default)
# AgentId, 2 for HA, 3 for Aduit
AGID = "CHANGEME"
# AppSecret
ASECRET = "CHANGE-ME"
# CorpID
CORPID = "CHANGEME"
sms_file = Path('/var/log/asterisk/sms.txt')
def send_notification(new_content):
print("Sending notification...")
# Sending Context
CONTEXT = f"{new_content}"
# Obtain Token Json Array
response = requests.get(f'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={CORPID}&corpsecret={ASECRET}', timeout=5)
JSON = response.json()
TOKEN = get_json_value(JSON, 'access_token', None).strip('"')
data = {
"touser": "@all",
"msgtype": "text",
"agentid": AGID,
"text": {"content": CONTEXT}
}
result = requests.post(f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={TOKEN}',
headers={"Content-Type": "application/json"},
data=json.dumps(data), timeout=5)
if result.status_code == 200:
print("Notification sent successfully.")
else:
print(f"Failed to send notification. Status code: {result.status_code}. Response: {result.text}")
def watch_sms_file(file_path):
last_position = file_path.stat().st_size if file_path.exists() else 0 # Ignore the existing content by starting from the end of the file
print("Starting to watch the sms file...")
send_notification("SMS Monitoring System Started")
while True:
file_path.touch(exist_ok=True) # Ensure the sms file exists
with open(file_path, 'r') as sms_file:
sms_file.seek(last_position)
new_content = sms_file.read()
if new_content:
print(f"New content detected: {new_content.strip()}")
send_notification(new_content)
last_position = sms_file.tell()
time.sleep(1) # Time interval to check the file again
if __name__ == "__main__":
watch_sms_file(sms_file)
新建一个服务:
$ systemctl cat sms-notify.service
# /etc/systemd/system/sms-notify.service
[Unit]
Description=SMS-Wechat Notification
After=network-online.target
[Service]
Type=simple
User=root
Restart=on-failure
RestartSec=15
ExecStart=/usr/bin/python3 /opt/send_notification.py
[Install]
WantedBy=multi-user.target
随后启用服务、开启自启动:
systemctl daemon-reload
systemctl enable --now sms-notify.service
一切正常的话你会收到一条提醒:
SMS Monitoring System Started
至此,短信部分配置完成。
配置 FreePBX和语音转发
添加分机号
下载安装 FreePBX,过程不表。值得一提的是如果Chrome没有弹出ISO下载,尝试用Edge。
完成初始设定后,添加两个分机号,一个用于接收端使用,一个用于测试手机调试使用。这里分别用801和802.
Applications – Extensions – Add Extension
注意Secret,这个相当于密码,后面在手机端注册需要用到。
其他保持默认设置即可。
添加Trunk
这里我们希望和光影猫的Asterisk建立trunk,而且不希望通过导入CID。如果自动导入CID,所有呼入来电均会变为移远的端口名。
进入FreePBX的SSH,增加以下配置:
[root@freepbx ~]# cat /etc/asterisk/extensions_custom.conf
[from-trunk-no-cidname]
exten => _X.,1,Set(CALLERID(name)="")
exten => _X.,n,Goto(from-trunk,${EXTEN},1)
[root@freepbx ~]#
回到网页端,点击 Connectivity – Trunks – Add Trunk
按如下设置
注意这里的Authentication被关闭,SIP Server为光影猫的地址,端口可以在这个文件查到:
$ cat /etc/asterisk/sip.conf
[general]
context=sip-default
udpbindaddr=0.0.0.0:46000
tcpbindaddr=0.0.0.0:47000
tlsbindaddr=0.0.0.0:5063
tlscipher=ALL
tlsclientmethod=tlsv1
accept_outofcall_message=yes
allow=!all,slin,ulaw,alaw
allowguest=no
allowtransfer=yes
alwaysauthreject=yes
.....
这里的 Context 我们设置为 from-trunk-no-cidname ,会进入上面的设置清洗掉CID。
配置路由
在 Connectivity – Outbound Routes设置呼出路由:
在Dial Patterns里我设置了两种外呼格式,分别是1开头的号码和9开头的号码。这样在呼出手机等1开头的号码时自动走移远模块。如果呼叫固话,则先打9再跟固话即可。(和之前一些酒店的设置一样)
在 Connectivity – Inbound Routes设置呼入路由:
配置FreePBX SIP
进入Settings – Asterisk SIP Settings,给RTP更改端口范围:
这里还把RTP Timeout改成3,原因是在外网呼叫电话时,挂断信号无法正常被转发,这里将设置为3秒无语音包后挂断电话。
进入 chan_pjsip页,开启TCP监听:
同时在下面把外网域名和外网IP写上。
保存,应用更改,重启服务器。
我发现有很多变更无法通过Web重启生效。任何涉及Asterisk的变更我都建议直接重启FreePBX服务器。
测试通话
此时基本配置就已经完成了,手机下载Zoiper,新建一个SIP Account,Domain写FreePBX的IP,Username写801,密码为上面设置的Secret。另一台则按照802的配置,两边都注册成功后,你就可以通过801,802这两个号码互相拨打电话了。
值得一提的是目前几款VoIP软件,比如Zoiper,SessionTalk的实现逻辑差异很大。在内网使用时没有太大区别,但我们还需要保证软件没有打开在前台时能正常接到推送通知(这里以iPhone为例),Zoiper为订阅制软件,SessionTalk为买断制,但Zoiper可以将RTP语音流一并进行转发,这能修正很多因NAT无法转发SIP而导致的问题。
我经过长时间测试,在下文提到的FRP方案中,没有找到一个不修改FRP源码就可以成功转发语音流的方案。所以最终使用Zoiper的订阅服务进行操作。
端口转发
进入前置机配置frpc的转发如下:
[freepbx_sip_port_udp_10000]
type = udp
remote_port = 10000
local_port = 10000
local_ip = 192.168.1.241
[freepbx_sip_port_udp_10001]
type = udp
remote_port = 10001
local_port = 10001
local_ip = 192.168.1.241
[freepbx_sip_port_udp_10002]
type = udp
remote_port = 10002
local_port = 10002
local_ip = 192.168.1.241
[freepbx_sip_port_tcp]
type = tcp
remote_port = 5060
local_port = 5060
local_ip = 192.168.1.241
local_ip为FreePBX的IP地址。重启frpc服务后,如果一切正常,你就可以从公网的5060端口注册到你的PBX服务器了。
如果你尝试拨打电话,正常来说是可以接通但是没有语音的。这里进入Zoiper的Settings,进入Incoming Calls,将Proxy Protocols选择为 SIP + RTP,再尝试拨打电话,就可以了。
最后,完成一切配置后,重启光影猫和FreePBX服务器,确保所有组件在重启后都能正常启动。
如果有监控系统的话,同时对光影猫的46000端口和FreePBX的5060端口做一个监听和告警,以免服务宕机影响使用:
小结
这个配置繁琐,且要求已有的基础设施众多,可能不适合大部分人使用。但是读到这里的人,想必也无需多说什么了。
祝你我向上走,有一份热,发一分光。
Reference
使用EC20模块配合asterisk及freepbx实现短信转发和网络电话
从0到1打造自己的VOIP网络电话系统(基于FreePBX)
有个问题想要问下楼主,我有个SIM只能开Wi-Fi calling才能通话/短信,这个情况下我研究了半天好像也没看到有支持的PCIE模块,是不是凉了
正是因为这种SIM需要Wi-Fi calling,我才想把他丢在家里环境中做成VOIP,可是好像目前文中的方案需要这张SIM本来就可以在蜂窝网络里通话才能托管?
不好意思忘记回复了。据我所知这种模块应该是不能支持WiFi calling的。
目前构思方案如下 :
1.购买PCIE 移远模块 EC2X (EC25),
2.Asteris 与FreePBX小米路由器编译openwrt.
3.N2N 内网组网。
请问一下,这样是否可行?
Asterisk和FreePBX的组合肯定是没有问题,没看出来openwrt在这里担当什么角色。
如果还需要内网组网才能连回来接打电话其实就失去实用价值了。