对接运营商SIP中继,我踩过的那些坑够写本书了
对接运营商SIP中继,我踩过的那些坑够写本书了

对接运营商SIP中继,我踩过的那些坑够写本书了

大概背景如下

上周接了个活儿:新接入一家运营商的SIP中继,号称是”标准对接”,结果呼入呼出全特么翻车。

说实话,我干了这么多年VoIP,对接过的运营商也不少了,从来没遇到过一个能一次性走通的。每家都说自己符合RFC,不信你去看他们的对接文档——那玩意儿要么过期了,要么写的和实际跑的根本不是一回事。

这次也不例外。

业务场景是这样的:

  • FreeSWITCH 1.10.7,跑在CentOS 7上
  • 需要同时支持呼入和呼出
  • 运营商那边给的文档写的是”SIP标准协议”,实际对接的时候发现他们的”标准”和我的”标准”不是一个标准

先说结论再吐槽:这次问题出在三个地方——

  1. Digest认证的realm匹配
  2. Contact头域的格式要求
  3. 号码E.164格式转换时机

每个都折腾了我至少半天,现在想起来还隐隐头疼。

第一个坑:Digest认证的realm

运营商那边发来配置,告诉我SIP中继用的是Digest认证。我心想这有啥难的,FreeSWITCH原生支持。

结果呢?

Authorization header is missing, cannot authenticate

Wireshark抓包一看,运营商回了 401,但WWW-Authenticate头里带的realm是”CC_SIP”,而我sofia配置里填的是运营商给的用户名。

这里有个关键点:有些运营商的SIP设备在Digest认证时,realm字段填的不是用户名,而是他们系统内部定义的域标识。如果你的配置里没有正确匹配这个realm,每次认证请求都会被拒。

检查一下你的 sofia.conf.xml 配置:

<profile name="carrier">

<settings>
    <param name="auth-calls" value="true"/>
    <param name="auth-realm" value="CC_SIP"/>
  </settings>

<gateways>
    <gateway name="carrier-gw">
      <param name="username" value="your_username"/>
      <param name="password" value="your_password"/>
      <param name="realm" value="CC_SIP"/>
      <param name="from-user" value="your_username"/>
    </gateway>
  </gateways>
</profile>

注意realm这个参数,很多人忽略了。运营商那边返回的401里WWW-Authenticate头是这样的:

WWW-Authenticate: Digest realm="CC_SIP", nonce="xxxx", qop="auth", algorithm=MD5

如果你的gateway配置里没有显式指定realm为”CC_SIP”,FreeSWITCH默认会用from-user作为realm去算摘要,结果两边算出来的MD5对不上。

技术判断:为什么我判断是realm的问题而不是密码错误?因为从抓包来看,运营商返回的是407而不是401。Proxy-Authenticate和WWW-Authenticate的区别意味着这可能是代理认证而非服务端认证,需要检查INVITE消息里的Proxy-Authorization头域。

但更直接的证据是:我手动用htdigest算了下摘要,确认密码没问题之后才怀疑到realm头上。

验证方法:

# 用openssl手动算一下Digest response,对比抓包里的值
printf "your_username:CC_SIP:your_password" | openssl md5 -hex

把计算结果和Wireshark里抓到的Authorization头里response字段比对,一致就说明密码没错,不一致就要检查realm。

第二个坑:Contact头域格式

认证问题解决之后,呼出能发起了,但是运营商那边说收不到我的BYE,呼叫一直挂在那儿转圈。

FreeSWITCH默认生成的Contact头是这样的:

Contact: <sip:mod_sofia@192.168.1.100:5060>

有些运营商(尤其是那些用华为或中兴设备的)会校验Contact头的格式,要求必须是用户侧的真实号码,而不是一个IP地址或者系统标识。

他们的逻辑是:如果Contact里填的是IP地址,那我怎么知道这个呼叫是从哪个分机发出来的?没法路由回话。

解决方案:在sofia gateway配置里加上 caller-id-in-from:

<gateway name="carrier-gw">
  <param name="caller-id-in-from" value="true"/>
  <param name="contact-params" value="transport=tcp"/>
  <param name="send-Contact" value="true"/>
</gateway>

或者更暴力的做法,在sip_profiles里直接改:

<param name="NDLB-force-contact" value="true"/>
<param name="NDLB-received-in-contact" value="true"/>

但是注意,这两个参数会影响全局行为,可能引起其他问题,所以最好只在特定gateway上配置。

方案取舍

  • 方案A:改caller-id-in-from,让Contact用主叫号码
  • 方案B:改NDLB-force-contact,用对方IP地址替换Contact
  • 方案C:直接和运营商沟通,让他们放行这种格式

我选了方案A,因为运营商那边说他们的设备支持配置白名单,放行IP或者特殊格式需要走工单审批,不知道要等多久。方案B有全局副作用,最后可能导致其他正常通话出问题。

第三个坑:号码格式——这是最让我崩溃的

呼入呼出都通了,但是!

呼出的号码有时候能通,有时候不能通。抓包一看,运营商那边收到的号码格式不一致。

问题在于E.164格式。

中国运营商的规范是:

  • 拨打国内号码:去掉+86,前缀加0086或者86
  • 拨打国际号码:保留+号,格式为+国家代码+号码

但很多系统设计的时候没考虑这个,或者转换时机不对。

FreeSWITCH的号码转换主要靠 dialplan 里的 regex 和 lua 脚本。常见的问题是:

  1. 出局的时候没有做E.164转换,号码还是本地格式(138xxxx),运营商那边无法识别
  2. 呼入的时候做了双重转换,比如已经转过的又被转了一遍
  3. 来电显示的号码没有转换,导致在软电话上显示为乱码

我的做法是在出局dialplan里统一做转换:

<extension name="outbound-number-translation">
  <condition field="destination_number" expression="^(\d{11})$">
    <!-- 国内手机号,转换为E.164格式 -->
    <action application="set" data="effective_caller_id_number=+86$1"/>
    <action application="set" data="destination_number=0086$1"/>
  </condition>
  <condition field="destination_number" expression="^(\d{10})$">
    <!-- 固话,考虑区号 -->
    <action application="set" data="destination_number=008610$1"/>
  </condition>
</extension>

但这还不够,因为我发现运营商那边对某些号段(比如95xxx、400xxx)的路由要求又不一样。

所以后来我把这些转换规则都抽出来,做成了一个lookup表:

-- /usr/local/freeswitch/scripts/number_translate.lua

local number_rules = {
  -- 格式:pattern -> {prefix, suffix, format}
  {"^1[3-9]%d%d%d%d%d%d%d%d%d$", "0086", "", "mobile"},
  {"^0[1-9]%d%d%d$", "0086", "", "landline"},
  {"^400%d%d%d%d%d%d%d$", "", "", "tollfree"},
  {"^95%d%d%d$", "", "", "special"},
}

local function translate_number(num)
  for _, rule in ipairs(number_rules) do
    local pattern, prefix, suffix, num_type = rule[1], rule[2], rule[3], rule[4]
    if string.match(num, pattern) then
      -- 根据号码类型做不同处理
      if num_type == "tollfree" then
        return "", num  -- 400/800号码直接发
      else
        return prefix .. num .. suffix, num
      end
    end
  end
  return num, num  -- 默认返回原号码
end

return translate_number

边界条件

  • 携号转网的号码处理:某些号段已经转网,但号码本身没变,运营商路由会走不同的网关
  • 虚拟号段的处理:物联网卡、部分企业号段的路由规则特殊
  • 国际漫游号码:手机在境外漫游时,主叫号码可能是+86开头,需要区分处理

验收标准:怎么证明我修好了

  1. SIP注册稳定性:连续注册24小时不断线,运营商侧看到的注册状态一致
# 检查注册状态
sofia status profile carrier gateway carrier-gw

# 应该看到 State: REGED 而不是 FAIL_WAIT
  1. 呼出通话:连续拨打50通国内电话,成功率100%
# 用originate命令批量测试
for i in {1..50}; do
  fs_cli -x "originate sofia/gateway/carrier-gw/138xxxxxxxx &echo()" &
done
wait
  1. 呼入通话:让运营商侧配合,连续呼入100通,成功率100%

  2. 抓包验证:用tshark过滤一下,确保所有SIP消息的头域格式符合预期

# 提取所有BYE消息的Contact头,确认格式正确
tshark -r capture.pcap -Y "sip.Method == BYE" -T fields -e sip.contact | uniq -c

复盘:这些坑的总结

  1. 运营商文档永远是参考,不要当圣经

    他们的技术支持工程师可能也搞不清楚自己的设备到底怎么配置的。我遇到过一个运营商,他们给的SIP中继配置文档里写的密码是错的,但技术支持一口咬定文档没问题。最后是我们直接打电话给他们的核心网团队,人家翻山越岭找了半天才发现配置在另一套系统里。

  2. 抓包是爹,有问题先抓包

    SIP相关的bug,80%以上靠Wireshark/tshark抓包就能定位。剩下的20%是运营商那边的问题,你抓了包甩给他们,他们也得认。

  3. 号码转换要做在边界,不要散落在各处

    早期系统设计的时候,号码转换逻辑可能散落在dialplan、lua脚本、甚至数据库里。后来维护的时候根本不知道哪个环节改了啥,导致有些号码被转了两遍,有些压根没转。

  4. 和运营商的技术对接要找对人

    一般的客服或者客户经理听不懂你说啥,他们只会说”我们已经记录了您的需求,会转交技术部门处理”。但技术部门处理的速度嘛……建议找那种能加微信直接沟通的,或者让他们拉个技术群,把核心网、网管、设备厂商的人都拉进来,不然一个问题能拖你一周。

  5. 备份配置!备份配置!备份配置!

    重要的事情说三遍。每次改配置之前,记得先备份原来的。运营商的设备有时候会发一些莫名其妙的消息,你的FreeSWITCH可能被带偏了,改回去的时候才知道哪个是有效的。

最后,FreeSWITCH是个好产品,但对接运营商这事儿,本质上是和一个不确定的系统博弈。能做的就是:多抓包,多记录,多沟通,少踩坑。

如果你们公司也要接SIP中继,祝你好运。真的。

发表回复

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

− 6 = 2