最近在项目中用 FreeSWITCH 对接运营商 SIP 中继,过程中踩了不少坑。把这些经验整理出来,希望能帮后来人少走弯路。
背景
项目需求是搭建一套企业呼叫中心系统,FreeSWITCH 作为核心媒体服务器,通过 SIP 中继连接运营商线路,实现外呼和接听 PSTN 电话。
坑一:注册模式 vs 点对点模式,一开始就选错了
刚拿到运营商给的对接文档,上面写着 SIP 服务器地址、用户名、密码,我想当然地用注册模式(register)去对接。配好 gateway 之后发现:注册状态一直是 TRYING,根本注册不上。
反复排查后才搞清楚------这家运营商用的是IP 鉴权(点对点模式),不需要注册。他们给的"用户名密码"其实是管理后台的登录凭据,不是 SIP 认证信息。
正确做法: 先跟运营商确认鉴权方式。如果是 IP 鉴权,gateway 配置里不需要填 register 相关参数,只需要配置 proxy 地址,并确保 FreeSWITCH 的出口 IP 已在运营商侧加白。
xml
<!-- IP 鉴权模式的 gateway 配置示例 -->
<gateway name="my_trunk">
<param name="realm" value="sip.carrier.com"/>
<param name="proxy" value="sip.carrier.com"/>
<param name="register" value="false"/>
<param name="caller-id-in-from" value="true"/>
</gateway>
坑二:NAT 导致单通、无声
这是 VoIP 领域最经典的坑,但每次遇到还是让人头疼。
症状是:呼叫能建立,但只有一端能听到声音,或者双方都无声。用 tcpdump 抓包一看,RTP 包的目标地址是内网 IP(比如 192.168.x.x),对端根本收不到。
排查思路:
FreeSWITCH 在 NAT 环境下,SDP 里携带的媒体地址默认是本机地址。如果服务器在内网且通过 NAT 出去,对端看到的是一个不可达的私有地址。
解决方案: 在 vars.xml 中配置外部 IP 和本地网段:
xml
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=你的公网IP"/>
<X-PRE-PROCESS cmd="set" data="external_sip_ip=你的公网IP"/>
<X-PRE-PROCESS cmd="set" data="local_ip_v4=内网IP"/>
同时在 SIP Profile(通常是 external.xml)中确认:
xml
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>
另外别忘了防火墙要放开 RTP 端口范围(默认 16384-32768/UDP),很多人只开了 5060 就觉得完事了。
坑三:主叫号码被运营商拒绝
外呼时 FreeSWITCH 日志显示 403 Forbidden 或 603 Decline。
原因是发出去的 SIP INVITE 里,From 头域的号码不在运营商允许的主叫号码池里。有些运营商对主叫号码管控很严格,你不能随便填。
解决方案: 在 dialplan 或 gateway 配置中显式设置合法的主叫号码:
xml
<!-- dialplan 中设置 -->
<action application="set" data="effective_caller_id_number=02812345678"/>
<action application="set" data="effective_caller_id_name=02812345678"/>
还有个细节:有些运营商要求主叫号码带区号但不带"0"前缀,有些又必须带。这种格式问题没有统一标准,只能跟运营商逐一确认。
坑四:编解码协商失败
呼叫建立后秒挂,日志里出现 488 Not Acceptable Here。
抓包看 SDP,发现 FreeSWITCH 提供了一堆编解码(PCMU、PCMA、G729、Opus...),但运营商只支持 PCMA(G.711A),而且对 ptime 也有要求,必须是 20ms。
解决方案: 在 gateway 或 dialplan 里限定编解码:
xml
<!-- 在 dialplan 里 bridge 之前设置 -->
<action application="export" data="absolute_codec_string=PCMA@20i"/>
或者在 gateway 配置中:
xml
<param name="codec-prefs" value="PCMA"/>
教训是:不要假设运营商支持多种编解码,对接前一定要确认支持的编解码列表和 ptime 要求。
坑五:DTMF 不生效
IVR 场景下,用户按键没有反应。FreeSWITCH 这边完全收不到 DTMF 信号。
DTMF 的传输方式有三种:RFC 2833(RTP 内带外)、SIP INFO、In-band(带内音频)。FreeSWITCH 默认用 RFC 2833,但运营商侧可能用的是 SIP INFO,两边不一致就收不到。
解决方案: 在 SIP Profile 中调整 DTMF 模式:
xml
<!-- 匹配运营商的 DTMF 方式 -->
<param name="dtmf-type" value="rfc2833"/>
<!-- 或者 -->
<param name="dtmf-type" value="info"/>
如果实在搞不定,可以尝试 start_dtmf application 来做带内检测,作为兜底方案:
xml
<action application="start_dtmf"/>
坑六:并发呼叫一多就出问题
少量呼叫测试一切正常,一上并发就出现各种奇怪现象:呼叫失败率飙升、音频断断续续、FreeSWITCH 进程 CPU 占满。
排查下来有几个原因:
-
文件描述符限制 :Linux 默认的 ulimit 太低,FreeSWITCH 处理大量并发连接时会耗尽。需要在
/etc/security/limits.conf中调大,同时修改 FreeSWITCH 的启动脚本。 -
数据库性能:FreeSWITCH 默认用 SQLite 存储通道信息,并发高了之后 SQLite 成为瓶颈。切换到 PostgreSQL 或关闭不必要的 CDR 写入可以显著改善。
-
RTP 端口耗尽:默认端口范围 16384-32768,大约 8000 个端口,每路通话需要至少 2 个端口(收发各一个)。如果并发超过 4000 路就可能不够用,需要扩大范围。
xml
<!-- switch.conf.xml 中调整 RTP 端口范围 -->
<param name="rtp-start-port" value="10000"/>
<param name="rtp-end-port" value="60000"/>
坑七:时间戳和计费对不上
运营商出的账单和我们系统记录的通话时长对不上,差个几秒到十几秒。
原因在于双方对"通话开始"和"通话结束"的定义不同。FreeSWITCH 的 CDR 中有好几个时间字段:created_time、answered_time、hangup_time、bridged_time,含义各不相同。运营商计费通常从收到 200 OK 开始,到收到 BYE 结束。
建议: 计费以 answered_time 到 hangup_time 的差值为准,并且在 CDR 中同时记录 SIP 信令的原始时间戳,方便和运营商对账时有据可查。
坑八:日志太多磁盘写满了
这个坑说出来有点丢人,但确实发生过。FreeSWITCH 默认日志级别是 DEBUG,在生产环境跑几天就把磁盘塞满了,然后整个服务直接挂掉。
解决方案: 生产环境调整日志级别为 WARNING 或 NOTICE,同时配置 logrotate:
xml
<!-- console.conf.xml -->
<param name="log-level" value="warning"/>
调试阶段可以临时开 DEBUG,但千万记得改回来。另外强烈建议把 SIP 抓包(pcap)和日志分开存储,用 tshark 或 sngrep 按需抓取,别用 FreeSWITCH 自带的 SIP trace 长期开着。
总结与建议
回头看这些坑,总结几条经验:
对接前一定要拿到运营商的详细技术文档,包括鉴权方式、编解码要求、号码格式规范、DTMF 方式、并发限制等。口头沟通的信息往往不够准确,白纸黑字最可靠。
sngrep 是调试 SIP 问题的神器,强烈推荐。它可以实时捕获 SIP 信令并以流程图的方式展示,比看原始日志效率高很多。遇到问题第一时间抓包,比猜来猜去强十倍。
搭建一套独立的测试环境,用 SIPp 等工具模拟运营商侧行为,把各种异常场景(超时、拒绝、编解码不匹配等)都跑一遍再上线。
最后,FreeSWITCH 的官方文档和社区虽然不算特别友好,但 wiki.freeswitch.org 和 GitHub Issues 里还是有很多有价值的信息,遇到问题值得翻一翻。
希望这篇记录能帮到正在踩坑的你。