如何把SIM卡从手机中取出

近年来,手机与个人私隐形成的强绑定让人诟病。上至健康码,下至一个普通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-chan-quectel

通过移远提供的项目,可以使得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实现短信转发和网络电话

关于sip server内网映射外网的一些记录

从0到1打造自己的VOIP网络电话系统(基于FreePBX)

Eliminate CallerID Name from inbound calls

基于coturn项目的stun/turn服务器搭建

“如何把SIM卡从手机中取出”的7个回复

  1. 有个问题想要问下楼主,我有个SIM只能开Wi-Fi calling才能通话/短信,这个情况下我研究了半天好像也没看到有支持的PCIE模块,是不是凉了
    正是因为这种SIM需要Wi-Fi calling,我才想把他丢在家里环境中做成VOIP,可是好像目前文中的方案需要这张SIM本来就可以在蜂窝网络里通话才能托管?

    1. 不好意思忘记回复了。据我所知这种模块应该是不能支持WiFi calling的。

  2. 目前构思方案如下 :
    1.购买PCIE 移远模块 EC2X (EC25),
    2.Asteris 与FreePBX小米路由器编译openwrt.
    3.N2N 内网组网。

    请问一下,这样是否可行?

    1. Asterisk和FreePBX的组合肯定是没有问题,没看出来openwrt在这里担当什么角色。
      如果还需要内网组网才能连回来接打电话其实就失去实用价值了。

  3. I’ve just bought EC20 and made it work but the voice quality was so bad. Too loud and has noise.
    Maybe AGC was misconfig.
    I read on other authors wiki that 16hz audio is not supported so dont expect good sound quality.
    How about your system?

    1. Hello, I did not encountered the same maybe it was due to VoLTE is enabled in my case. You may try to split the trouble into ‘Asterisk’ part and ‘FreePBX + End User Software’ part, see which part is leading to this.

      1. Hi, thanks.
        I’ve test softphone to softphone and it was good.
        Only from LTE module to Asterisk has noise, UAC was enable btw.
        “`
        ; quectel required settings
        [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
        “`
        I’ll look about VoLTE again.

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注