FreePBX解决动态公网IP环境SIP地址更新问题

Asia/Shanghai | Leave a comment
FreePBX解决动态公网IP环境SIP地址更新问题
FreePBX解决动态公网IP环境SIP地址更新问题

This post is over a year old, some of this information may be out of date.

记得很久以前看过一个留学生使用华为上网棒架设越洋VOIP服务器实现境外使用国内SIM卡拨打电话的文章,颇为有意思,操作简单来说就是购买华为特定型号的USB上网棒(Dongle Modem),手机卡使用特定运营商的SIM,当然这里选择最便宜的套餐,国内使用树莓派安装RasPBX系统,将上网棒接入树莓派,配置FreePBX之后就可以愉快使用树莓派接打电话或收发短信了,而且在地球的任意角落也可以通过互联网实现境内通话资费。

一直对这种新鲜Geek范儿的事情比较感兴趣,正好手头闲置有树莓派,于是动手实施,当然本着不折腾不舒服的理由,我没有直接安装RasPBX这个定制的系统,当然另外的考虑就是树莓派仅作为话务中心服务对于我来说没有充分榨干其价值,毕竟上面还跑有其他各种特殊的服务,于是参考了RasPBX的定制脚本开始了折腾之路......遗憾的是这个事情已经过去有六七年了,当时没有记录折腾的步骤,虽然配置成功了,但因为FreePBX这个系统太占资源了,导致树莓派不堪重负(其间还配了大流量电源),后来我停掉了IVR和通话功能,仅保留了接收短信和发送短信的功能,于是这个电话卡就用来自己注册接收短信用。

再后来某天我整理弱电箱的奇奇怪怪的设备,发现了这个吃灰的玩意儿,想到其默默工作了这些年也该提档升级发挥最大功能了,考虑到FreePBX的高资源占用,这次我将FreePBX移到了x86 mini主机上,上了固态和DDR4,完全满足FreePBX对资源的渴求,当然现在也不像以前那么爱折腾了,正好近两年来我大部分网络设施都Docker化,于是顺水推舟,Asterisk+FreePBX+MySQL什么的全部移入Docker中,这里使用的是tiredofit/docker-freepbx,当然做了修改,尤其是chan_dongle.so对于中文支持问题,使用wdoekes/asterisk-chan-dongle,解决at_response_cmgr: [dongle0] Error parsing incoming message: Cannot parse UCS-2错误。

1 NAT导致通话声音的问题

如果一切顺利的话其实也就没有这篇文章了,刚开始配置确实顺利调试通过,接打电话也完全OK,当然这之前有小插曲,那就是接打电话没有声音和30秒挂断,后来发现是防火墙仅开通了5060和5061端口,但没有打开UDP10000-20000的端口转发,语音是需要这个端口范围的。接打电话OK,短信OK,难道事情如此顺利?非也非也,第二天当我满怀欣喜的去单位炫耀我最新成果时出现了尴尬的场面,同事电话打过来,接听没有声音,当然我说话对方也无法听见,这就让人当场社死了。

1.1 问题根源

回去尝试解决这个问题,最终搜索网络发现是NAT配置问题,在Settings->Asterisk SIP Settings->General SIP Settings配置菜单里面,NAT Settings栏目里有个External Address,这个获取的地址与实际IP不符,需要重新点击Detect Network Settings获取最新地址并提交,再Apply Config才能修复问题。我知道这是因为宽带运营商(ISP)定期刷新IP池所导致的,当然宽带运营商能够大发慈悲的给我提供动态公网IP我已经很感谢了,不指望这个IP能够固定下来(运营商:固定IP加钱可得),但我总不能时刻检查IP地址变动并手动更新这里配置吧,难道不支持DDNS吗?

1.2 尝试DDNS

实际上General SIP SettingsExternal Address的配置应用的是chan_pjsip模块,切换到SIP Settings [chan_pjsip]就能看到相关设置,可能读者注意到选项卡旁边还有chan_sip模块的配置(SIP Legacy Settings),点进去看到了Dynamic Host,这个不是我正需要的DDNS吗?但是很遗憾chan_pjsip并不支持,我尝试切换到chan_sip模块,但我的SIP终端又出现了问题。

其实FreePBX对于chan_pjsip是支持DDNS的,但其官方配置说明仅限于商业版本,购买昂贵的商业版本对于我这个小众爱好者明显不太现实,那么我抖机灵的将External Address填成DDNS主机是否可以呢?

答案是可以是可以但不确定是否有效,我并没有找到相关说明表示这里支持DDNS主机, 但搜索网络让我有了新的发现。

【2022年2月22日更新】新版本的Asterisk的pjsip是支持DDNS的,具体可以参考官方博客《PJSIP: DNS Manager (dnsmgr) and Full Dynamic Hostname Support, Coming Soon!》也就是说External Address填成DDNS主机是完全OK的,但是这个有个前提就是最新版本且开通了DNS Manager (dnsmgr) ,可以通过asterisk -rvvvvvv连接后使用dnsmgr status查看状态:

CLI> dnsmgr status
DNS Manager: enabled
Refresh Interval: 300 seconds
Number of entries: 0

如上所示,出现enabled说明已经启用了DNS Manager,如果没有启用可以通过编辑/etc/asterisk/dnsmgr.conf文件为以下内容启用:

[general]
enable=yes
refreshinterval=300

这里refreshinterval表明刷新IP的最小间隔,完成配置后重启asterisk服务就可以了,如果是老版本系统或者不支持dnsmgr,可以继续看下去。

2 使用定时任务运行Bash Shell脚本自动配置

2.1 Bash Shell自动化脚本

为什么不直接通过cron定时任务定期检测IP地址并自动更新External Address栏呢?TECHNOTES这篇文章《A Bash script to rewrite the "static" IP address in the FreePBX Asterisk SIP Settings when it is changed by your ISP》给出了一个方案,当然考虑到实际情况我对其脚本进行了修改以适配最新的FreePBX 15,修改后的脚本如下:

#!/bin/bash
# This program gets the current IP address (as assigned by the ISP) from
# OpenDNS and modifies the FreePBX Asterisk SIP settings if the external IP
# address has changed. Invoke it as cron job that runs every 5 minutes.
# THIS SCRIPT IS STILL CONSIDERED EXPERIMENTAL - USE AT YOUR OWN RISK!!!
user="asterisk"
pass="yourpass"  # 改为你的asterisk数据库密码
host="localhost" # 改为你的数据库主机名或者地址

check=$(dig +short myip.opendns.com @resolver1.opendns.com) || { 
   check=$(curl -4s http://checkip.amazonaws.com) || { echo "Problem getting current IP address"; exit 1; }
}

if [[ ! $check =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  echo "Invalid IP address"; exit 1;
fi

readip=$(mysql asterisk -u $user -p$pass -h$host -se 'SELECT val FROM kvstore_Sipsettings where `key` = "externip"') || { echo "Can't read externip from MySQL asterisk database"; exit 1; }
if [ "$check" != "$readip" ]; then
    # IP address has changed
    echo "It appears that PBX external IP address has been changed to $check, old IP address $readip will be replaced."
    mysql_response=$(mysql asterisk -u $user -p$pass -h$host -se 'update kvstore_Sipsettings set val='\"$check\"' where `key`="externip" ')
    # Reload Asterisk
    #/var/lib/asterisk/bin/module_admin reload
    /usr/sbin/fwconsole reload
fi
exit 0

注意上面脚本的userpasshost需要修改为你的正确配置,如果不清楚,可以查看/etc/amportal.conf或者/etc/freepbx.conf配置文件的AMPDBUSERAMPDBPASSAMPDBHOST内容。另外刷新配置这块需要注意,原文是采用/var/lib/asterisk/bin/module_admin reload方式,我这里注释掉了并使用新版本的刷新命令/usr/sbin/fwconsole reload,各位根据实际情况取舍吧。

有网友反映这里设置不能实时更新到asterisk,具体表现为每次脚本执行IP变更后,仍然需要手动重启asterisk服务service asterisk restart,不然设置不能生效,即使执行了fwconsole reload也不行,有这个问题的朋友可以尝试开启SIP Settings [chan_pjsip] -> Allow Transports Reload选项,这样在脚本执行fwconsole reload的时候,Transports也能随之更新(此段2022年2月22日更新)。

2.2 配置定时任务

将2.1节脚本保存为/var/lib/asterisk/agi-bin/updateip.sh并运行chmod +x /var/lib/asterisk/agi-bin/update.sh赋予执行权限,最后执行定时任务crontab -e,插入下面一行:

*/5 * * * * /var/lib/asterisk/agi-bin/update.sh >/dev/null 2>&1

这里设定为每个5分钟执行脚本,如果外网IP改变则立即重新配置FreePBX和刷新Asterisk。

2.3 tiredofit/docker-freepbx的定时任务配置

对于tiredofit/docker-freepbx需要注意的是自己下载官方Docker,找到install文件夹,并在其中assets文件夹内创建cron文件夹,cron文件夹里建立文本文档crontab,完整的路径是./install/assets/cron/crontab,修改crontab并插入2.2节的定时任务,保存文件并docker build即可(记得把2.1节updateip.sh脚本也编译进镜像,否则cron会找不到执行程序)。

3 总结

整体上FreePBX对Asterisk提供了友好易用的Web GUI,而且FreePBX对于社区版本开源并开放了大部分功能,这些功能对于我们个人来说足够,即使有特别的需求也可以通过其他方式进行Hack。

注:Asterisk和FreePBX是Sangoma Technologies公司注册商标。