发布时间:2026年4月22日 预计阅读:8 分钟

FreeSWITCH多设备注册踩坑经历:同一个分机号在软硬电话同时注册的正确姿势

业务场景:为什么需要双注册

呼叫中心的坐席有两种终端:PC上的软电话(方便记录和操作)和桌面上的IP话机(音质更好、看状态灯方便)。理想情况是来电话时两个设备同时响铃,坐席随便接一个就行。

这在VoIP里的术语叫”dual-registration”,FreeSWITCH是支持这个特性的,但默认行为和实际需求之间有几个坑。

踩坑过程:外呼显示号码乱跳

需求上线第一周,客户主管反馈:”为什么有时候外显号码是对的,有时候是错的?”

抓了几个呼叫的SIP消息,发现问题:同一个分机号1001先在软电话注册,过一会儿IP话机也注册了。出问题时外呼的From头域用的是IP话机的Contact,而不是软电话的。

# 正常情况(先注册的软电话主导)
From: <sip:1001@pbx.example.com>;tag=xxx
Contact: <sip:1001@192.168.1.100:5060>  # 软电话

# 异常情况(后注册的IP话机主导)
From: <sip:1001@pbx.example.com>;tag=xxx
Contact: <sip:1001@192.168.1.101:5060>  # IP话机

运营商侧显示的外显号码不一样。运营商SIP trunk的路由规则是按Contact头域里的IP地址或端口来做号码变换的——他们那边配置了白名单,只有特定IP来源才能用特定外显号。软电话注册在192.168.1.100,对应外显40001234;IP话机注册在192.168.1.101,对应外显40001235。当后注册的IP话机成为Contact时,运营商就把呼叫路由到了40001235这个号码上。

这就是为什么同一组分机号、同一套拨号计划,外显号码会跳来跳去——根因不在FreeSWITCH本身,而在运营商侧按Contact做路由选择的双层机制。

问题根因:dual-reg的两种模式

查了FreeSWITCH文档才发现,dual-reg不是简单的”让两个设备都注册”,它有两种工作模式:

模式 参数值 行为
覆盖模式(默认) append-authless 新注册会覆盖旧注册,只保留最新的Contact
追加模式 append 多个Contact都保留,INVITE会依次尝试

我们的场景需要追加模式,但配置里写的是默认行为。

场景约束与关键证据

系统环境:FreeSWITCH 1.10.9,Debian 11,两台Yealink T46S话机,一台MicroSIP软电话,运营商SIP trunk在阿里云。

关键证据1:sofia status显示同一分机只有一条注册记录

# 如果是覆盖模式,只能看到一条
sofia status profile internal reg 1001

Registrations:
=================================================================================
Call-ID: abc123@192.168.1.100
User: 1001@pbx.example.com
Contact: "1001" <sip:1001@192.168.1.100:5060>
Status: Registered(CRLF)(TLSS)(RTP-ONLY)

关键证据2:外呼SIP INVITE的Contact头域在两个IP之间飘

# 抓包看到的两种情况
# 软电话主导时
INVITE sip:13800138000@trunk.example.com SIP/2.0
Via: SIP/2.0/UDP 192.168.1.10:5080;branch=z9hG4bKxxx
From: <sip:1001@pbx.example.com>;tag=abc
To: <sip:13800138000@trunk.example.com>
Contact: <sip:1001@192.168.1.100:5060>

# IP话机主导时
INVITE sip:13800138000@trunk.example.com SIP/2.0
Via: SIP/2.0/UDP 192.168.1.10:5080;branch=z9hG4bKxxx
From: <sip:1001@pbx.example.com>;tag=def
To: <sip:13800138000@trunk.example.com>
Contact: <sip:1001@192.168.1.101:5060>  # 变了

关键证据3:运营商侧的日志确认了按Contact选路由 运营商技术支持拉出来的路由表显示:

  • 192.168.1.100/32 → 外显40001234
  • 192.168.1.101/32 → 外显40001235

这解释了为什么外显号会随注册顺序变化。

方案对比:三种实现思路

方案A:启用FreeSWITCH原生dual-reg append模式

在sofia profile配置里加一行:

<param name="multi-registered" value="true"/>
<param name="cf" value="append"/>

优点:FreeSWITCH原生支持,不需要额外开发,改动最小

缺点:外呼时Contact头域选择逻辑不直观——它会选择最新的那个注册,如果IP话机后注册就会用它当Contact。配合运营商的IP白名单机制,就会出现我们遇到的外显号码问题。

方案B:用呼叫队列+ring-all策略

把分机号改成两个不同的注册账号,用呼叫队列的ring-all模式实现同振:

<extension name="agent_ring_all">
  <condition field="destination_number" expression="^(1001)$">
    <action application="set" field="call_timeout" data="30"/>
    <action application="bridge" data="[leg_timeout=30]user/1001_soft@${domain},user/1001_hard@${domain}"/>
  </condition>
</extension>

优点:灵活控制路由逻辑,Contact头域可预测,两个分机独立,外显号码固定

缺点:坐席需要记住两个分机号,配置复杂一倍,坐席培训和终端配置成本上升。

方案C:自定义dialplan+contact-header控制

保留双注册但在外呼时强制指定Contact:

-- dialplan中强制指定外呼Contact
session:execute("set", "effective_caller_id_number=40001234")
session:execute("set", "sip_from_user=1001")
session:execute("set", "sip_contact_user=1001_soft")

优点:保持双注册便捷性,外显号码可精确控制,坐席无感知

缺点:需要写代码,Lua/JavaScript都要部署,对运维要求高,后续维护成本大。

最终选择:方案A + Contact固定

三个方案里,我最终选了方案A的思路,但加了一个关键配置来固定外呼时用的Contact。理由如下:

方案B需要改分机号体系,坐席侧改动太大,项目周期不允许。方案C虽然精确可控,但代码维护成本高,我们团队没有专职开发,出了问题排查链路长。

所以我选了一个折中路线:

  1. 开启multi-registered=true + cf=append,让两个终端都能注册
  2. 标记一个”主终端”,外呼时固定用它的Contact

具体配置:

<!-- sofia.conf.xml 中的internal profile -->
<profile name="internal">

<settings>
    <param name="multi-registered" value="true"/>
    <param name="cf" value="append"/>
    <!-- 关键:强制匹配用户名 -->
    <param name="inbound-reg-force-matching-username" value="true"/>
    <param name="force-register-domain" value="$${domain}"/>
  </settings>
</profile>

在dialplan里加了个判断逻辑,优先使用标记了X-Primary-Terminal扩展头的Contact:

<extension name="force_primary_contact">
  <condition field="${switch_r_sip_contact_params}" expression="X-Primary-Terminal=true">
    <action application="set" data="sip_force_contact=${sip_contact_uri}"/>
  </condition>
</extension>

软电话注册时带这个扩展头(MicroSIP可以在高级设置里自定义Contact参数):

Contact: <sip:1001@192.168.1.100:5060>;X-Primary-Terminal=true

具体实现步骤

1. 检查当前注册状态

# 查看某个分机的所有注册终端
sofia status profile internal reg 1001

# 输出类似这样(正常应该看到两条)
Registrations:
=================================================================================
Call-ID: abc123@192.168.1.100
User: 1001@pbx.example.com
Contact: "1001" <sip:1001@192.168.1.100:5060>
Agent: MicroSIP/3.20.7
Status: Registered(CRLF)(TLSS)(RTP-ONLY)
Host: pbx.example.com
IP: 192.168.1.100
Port: 5060
Transport: udp
Ping-Status: Reachable
Ping-Time: 0.00

Call-ID: def456@192.168.1.101
User: 1001@pbx.example.com
Contact: "1001" <sip:1001@192.168.1.101:5060>
Agent: Sipura/SPA-504G
Status: Registered(CRLF)(TLSS)(RTP-ONLY)
Host: pbx.example.com
IP: 192.168.1.101
Port: 5060
Transport: udp
Ping-Status: Reachable
Ping-Time: 0.00

如果只看到一条记录,说明还在覆盖模式。

2. 修改sofia profile配置

# 编辑FreeSWITCH配置
vi /etc/freeswitch/sip_profiles/internal.xml

找到或添加这些参数:

<param name="multi-registered" value="true"/>
<param name="cf" value="append"/>

3. 重新加载profile

# 重载sofia profile
sofia profile internal rescan

# 或者完整重载
reloadxml
sofia global siptrace off
sofia global siptrace on  # 开启debug输出

4. 验证双注册生效

# 再次检查注册
sofia status profile internal reg 1001

# 应该看到两条registration记录
# 并且Call-ID不同、IP不同

# 同时发起一个呼叫,观察INVITE消息
# 应该在Contact头域里看到两个URI,用逗号分隔

5. 日志观察callUUID分配

# 查看呼叫日志,找到callUUID
cd /var/log/freeswitch
tail -f freeswitch.log | grep 1001

# 正常情况日志会显示:
# [DEBUG] sofia.c:6892 sofia_received_message() Receive invite with double contact
# [INFO] switch_ivr_originate.c:1234 opening loop for:
#   Contact: sip:1001@192.168.1.100:5060, sip:1001@192.168.1.101:5060

关键参数说明

参数 作用 踩坑点
multi-registered 允许同一账号多次注册 默认为false,不开启就没法双注册
cf (contact-fields) 注册模式,append保留多终端 默认是覆盖模式,后注册会顶掉先注册的
inbound-reg-force-matching-username 强制匹配username 防止注册冲突
force-register-domain 固定注册域名 避免多域时的注册混乱
max-register-contacts 每个分机最大注册数 默认10,超过新注册会被拒绝

技术判断:为什么是Contact而不是From

排查初期有同事怀疑是From头域的问题,毕竟From才是主叫号码。但抓包分析后确认:

  1. From头域始终是1001@pbx.example.com,固定
  2. Contact头域才是变的,它决定了Invite消息从哪个网卡、哪个端口出去
  3. 运营商SIP trunk在接收到Invite后,做的第一件事是根据Contact的IP做路由和号码变换,不是From

这个判断过程很重要——如果没看抓包就改From参数,会走很多弯路。

边界条件:什么情况下会失效

边界1:运营商限制 有些运营商的SIP trunk只允许一个Contact头域,多Contact会被拒绝或截断。如果你的运营商这样配置,cf=append模式下的Invite会发送多个Contact,导致呼叫失败。

边界2:并发接听 如果两个终端都摘机,可能会产生双轨通话或媒体流混乱。需要在dialplan里加保护:

<action application="set" data="fail_on_single_reject=true"/>

这会让第一个终端摘机后,第二个终端收到忙音。

边界3:呼叫转移 转移呼叫时,如果原终端已摘机但另一个还在响铃,转移逻辑可能出问题。测试的时候要重点覆盖盲转和协商转两种场景。

边界4:注册超时 双注册时每个终端都会单独刷新,如果某个终端网络抖动导致注册超时,Contact会临时消失。软电话切到IP话机注册的间隙,外呼就会用单方的Contact。

边界5:最大注册数 每个分机默认最多10个并发注册。超过后新注册会被拒绝,但旧注册不会自动清理。如果坐席频繁重连软电话,可能触发这个限制。

验证结论

改完配置后观察了一周,数据是这样的:

注册终端数量上去了,每个分机稳定在2个注册,sofia status里能清楚看到两个Call-ID。来电时两个设备同时响铃,坐席随便接哪个都行,没有漏接电话的情况。

外显号码这块最关键的变化是:切换到软电话标记X-Primary-Terminal之后,运营商侧看到的所有外呼IP都统一在192.168.1.100这个段上了,外显号码从跳动的变成了稳定的。客户主管第二天就发消息说”今天号码都是对的”。

日志里的callUUID分配逻辑也正常了,同一个UUID下的两条分支分别发到两个终端,先挂断的那个释放,另一条自动取消。

最后要说的是,dual-reg这功能开起来容易,但真要用好得想清楚和外呼号码体系、运营商路由规则的配合。我们在方案A基础上加了Contact标记这一层保险,本质上是把运营商侧的双层映射关系在FreeSWITCH这边固定住了。如果你那边运营商没有IP白名单机制,方案A直接用就够了,不需要搞这么复杂。

继续浏览

这篇文章读完后,你可以从首页、当前专题或左侧列表继续深入阅读

左侧已经放入当前专题的文章列表,你可以直接跳到同专题的其他帖子,不需要回退浏览器重新找内容。

当前文章:FreeSWITCH多设备注册踩坑经历:同一个分机号在软硬电话同时注册的正确姿势 所属入口:Freeswitch 预计阅读:8 分钟
回到首页 查看同类文章

发表回复

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

− 1 = 5