简介:《freeswitch呼入呼出路由配置详解》是一份面向VoIP运维工程师、通信开发人员及系统集成商的实用文档,围绕Freeswitch在真实网络环境中的呼入呼出路由配置和SIP中继调试展开深入讲解。文档从事件驱动架构切入,首先厘清了拨号计划对电话号码路由规则的定义方式,随后分别说明呼入侧如何将外部呼叫转接至SIP中继、语音邮箱或分机,以及呼出侧如何通过PSTN网关、SIP中继或对等中继模式发起呼叫,并对中继地址、端口、认证信息、传输协议等核心参数给出配置指引。同时,文档还总结了安全性、负载均衡、错误处理、日志监控等上线前必须关注的实施要点,帮助读者避开常见配置陷阱。资源包共1个文件,格式为doc,大小约221KB,内容以FreeSwitch V1.2.7系统介绍为主线,涵盖项目背景、CORE启动与消息分发、MOD_SOFIA模块组成等章节,结构清晰,由浅入深。已有6446人学习下载,无论对于刚开始接触Freeswitch路由配置的初学者,还是希望完善现有中继设置的运维人员,都有较强的参考价值,尤其适合在企业语音组网与呼叫中心场景中快速落地。
1. 呼入呼出路由,先想清楚一个前提
很多团队把 FreeSWITCH 当成一个“能注册、能打电话”的黑盒,结果到了配置路由这一步就卡住了。最常见的情况是:话务能进到 FreeSWITCH,但从网关呼出的那一侧要么号码不成立,要么被叫侧看到的主叫完全不对;或者分机互拨正常,外部呼入却总是被丢进默认提示音。这些问题的根子大多不在 SIP 信令,而在路由上——FreeSWITCH 默认的 demo 配置能打内线,但永远做不到直接上生产。
这篇把呼入呼出路由配置这套东西拆开讲。先立一个前提:呼入路由负责“外部电话打到哪个 IVR、哪个分机”,呼出路由负责“分机拨号后走到哪个网关、号码怎么变换”。两者共用一套 dialplan(拨号方案),但入口不同。呼入由 SIP profile 的context参数决定进哪个上下文,呼出由分机所在 directory 里的context参数决定,也可以由显式路由脚本整体接管。适合谁:如果你已经在跑单机 FreeSWITCH,或者正从 WebRTC 演示往正式呼叫中心、会议系统迁移,这篇文章能帮你把路由逻辑理清,并且可以直接抄配置。我默认你用的是 1.10 或更晚的版本,XML dialplan 的语法在这两个版本里没有破坏性变化。
2. 从 XML dialplan 看懂路由的匹配顺序
FreeSWITCH 的路由核心是 dialplan,它由context、extension、condition、action四层组成。一条呼叫进来后,FreeSWITCH 根据呼叫方向确定入口 context,然后在这个 context 内部从上到下逐条匹配 extension,extension 内部再逐条匹配 condition。任何一层匹配失败,呼叫都会落进 context 默认的 no-match 处理,通常就是拒绝或者放音。
2.1 context 是路由的第一道闸门
context 是 FreeSWITCH 里的逻辑容器,可以理解成一张独立的拨号计划表。不同来源的呼叫被导向不同 context,就是在做第一次路由分流。生产上我通常会拆出这几个 context:default(分机互拨和出局)、from-pstn(外部呼入)、from-did(按中继或 DID 号码再细分)、public(只留匿名访问出口,不放业务)。
看一个最小呼入 context 的 XML:
<context name="from-pstn"> <extension name="main-ivr"> <condition field="destination_number" expression="^(\d+)$"> <action application="transfer" data="main_menu XML default"/> </condition> </extension> </context>这里destination_number是 FreeSWITCH 内置的拨号变量,表示被叫号码。正则^(\d+)$匹配纯数字串,能过滤掉一部分非法 URI。transfer动作把呼叫转到default上下文里名为main_menu的 extension,XML表示去 XML dialplan 里找。这样写的好处是:呼入的逻辑统一收敛在from-pstn上下文,后续要加黑名单、白名单、时间路由,都往这个 context 里塞 extension 就行。
2.2 extension 的匹配受 break 参数控制
很多人以为 dialplan 是“匹配到就停”,其实不完全是。FreeSWITCH 的匹配行为由break参数决定,它在 extension 和 condition 两个层级都能出现。默认情况下,一个 extension 内部所有 condition 都匹配成功,且 action 执行完后路由就停了。但如果某个 condition 失败,是否继续尝试下一个 extension,要看 break 的取值。
| break 取值 | 位置 | 实际行为 |
|---|---|---|
| 不写(默认) | extension/condition | 匹配成功就停在当前 extension |
on-false | extension | 当前 extension 有 condition 失败时,继续尝试下一个 extension |
on-true | extension | 当前 extension 匹配成功也继续尝试下一个 extension |
never | extension | 强制只匹配当前 extension,不往下走 |
我一般只在需要“先试分机、不通再走溢出路由”的场景里用on-false。日常的呼入呼出路由配置不需要频繁改 break,但你要知道它的存在,否则排错时看到日志里“路由匹配成功却没有停止”会一头雾水。
condition字段也能拆成多行,多个 condition 之间默认是“与”的关系,全部为真才执行 action。如果希望某个 condition 为假时走另一段逻辑,用break="on-false"加一个只有anti-action的 condition 是常见做法。
2.3 先写一个能满足多数场景的拨号方案
下面这段 XML 是生产环境里比较稳的起步配置,按“分机互拨、出局去 0、外部呼入接 IVR”三个维度划分。为了少踩坑,我把default和from-pstn分开写,而不是揉在一个上下文里。
<context name="default"> <!-- 分机互拨:分机号段 10xx --> <extension name="internal-extension"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="bridge" data="user/${destination_number}@${domain_name}"/> </condition> </extension> <!-- 出局路由:0 开头走主网关,去掉前导 0 再转发 --> <extension name="outbound-local"> <condition field="destination_number" expression="^0(\d+)$"> <action application="bridge" data="sofia/gateway/gw-main/$1"/> </condition> </extension> <!-- 兜底:找不到路由就放忙音 --> <extension name="no-match"> <condition field="destination_number" expression="^.*$"> <action application="playback" data="tone_stream://%(400,200,480)"/> </condition> </extension> </context> <context name="from-pstn"> <!-- 外部呼入统一先进 IVR --> <extension name="main-ivr"> <condition field="destination_number" expression="^(\d+)$"> <action application="transfer" data="main_menu XML default"/> </condition> </extension> </context>sofi/sofia/gateway/gw-main/$1里的$1是正则捕获组,把0后面的号码提取出来再送到网关注册名gw-main。这样写的好处是号码变化逻辑一目了然:用户拨01012345678,实际到网关侧的是1012345678。tone_stream://%(400,200,480)是 FreeSWITCH 内置的忙音生成串,兜底场景用它比放一段录音更直接。
2.4 路由没生效时先看决策日志
配完 dialplan 最常见的疑问是“为什么没走我写的路由”。FreeSWITCH 里最快的确认方式是开控制台日志,然后在 condition 里临时加一行log动作。
/usr/local/freeswitch/bin/fs_cli -x "console loglevel info"再往拨号方案里加一行:
<condition field="destination_number" expression="^(\d+)$"> <action application="log" data="INFO 命中呼入路由,被叫号码=${destination_number}"/> <action application="transfer" data="main_menu XML default"/> </condition>呼叫一次,如果控制台没有出现这行日志,说明呼叫根本没进这个 context,问题出在 SIP profile 或 Sofia 网关的context参数上,而不是 dialplan 本身。这个判断顺序能省下大量无效排错时间。
3. 呼入路由配置:从 SIP profile 到具体分机的链路
呼入路由的配置关键不在 dialplan,而在“呼叫到底进了哪个 context”。SIP profile 里默认的 context 是public,这是 FreeSWITCH 预置的演示上下文。生产环境必须把它改成自己的 context,否则等于把匿名呼叫直接放进公共区域。
3.1 SIP profile 与网关的 context 参数要分清
呼入来源一般有两类:一类是运营商中继直接打到 FreeSWITCH 的 SIP profile,另一类是上游 IPPBX 或 SBC 通过 Sofia 网关把话务转过来。这两类的入口 context 配置位置不一样。
SIP profile 的 context 在sip_profiles配置里,比如external.xml:
<profile name="external"> <param name="context" value="from-pstn"/> <param name="inbound-codec-prefs" value="PCMU,PCMA,G722"/> </profile>Sofia 网关的 context 在外呼网关配置里,比如gw-main.xml:
<gateway name="gw-main"> <param name="username" value="route-01"/> <param name="password" value="secret"/> <param name="proxy" value="203.0.113.10"/> <param name="context" value="from-did"/> </gateway>两者的区别:SIP profile 管的是“直接落在本机监听端口上的呼入”进哪个 context,Sofia 网关管的是“上游通过该网关账号呼入”进哪个 context。很多时候分机呼出正常、外线呼入不对,就是因为网关里没写 context,FreeSWITCH 用了默认的public。
参数说明:
| 参数 | 位置 | 作用 |
|---|---|---|
context(profile) | SIP profile | 设置直接呼入到本端口的默认入口 context |
context(gateway) | Sofia 网关 | 设置该中继呼入的入口 context,优先级高于 profile 默认值 |
inbound-codec-prefs | SIP profile | 影响呼入侧协商的编码顺序,G722 优先可改善语音质量 |
3.2 呼入字段匹配:用 DID 和主叫号段做二次分流
进入from-pstn后,多数场景还要做二次分流:不同 DID 转不同 IVR、特定主叫号段直接转分机、黑名单拦截。这些都是在一个 context 里用多个 extension 实现,匹配字段用destination_number和caller_id_number。
<context name="from-pstn"> <!-- 服务热线 DID 进客服队列 --> <extension name="did-support"> <condition field="destination_number" expression="^4008888888$"> <action application="transfer" data="support_queue XML callcenter"/> </condition> </extension> <!-- 黑名单主叫:直接挂断 --> <extension name="block-caller"> <condition field="caller_id_number" expression="^13800138000$"> <action application="hangup" data="CALL_REJECTED"/> </condition> </extension> <!-- 其余 DID 走统一 IVR --> <extension name="default-ivr"> <condition field="destination_number" expression="^(\d+)$"> <action application="log" data="INFO 命中默认呼入路由,DID=${destination_number}"/> <action application="transfer" data="main_menu XML default"/> </condition> </extension> </context>这里把黑名单放在 DID 分流之前,是因为呼入路由的匹配顺序是从上到下,第一个命中的 extension 会优先执行。hangup的挂断原因CALL_REJECTED在呼叫详细记录里可见,便于后续排查恶意呼叫。
3.3 呼入后的早媒体与 TLS 校验设置
运营商网关对接时,有两个参数容易被忽略:p-early-media-support和tls-verify-policy。前者控制是否把对端的 183 Session Progress 当作早期媒体转发给主叫侧,后者控制 TLS 中继场景下是否校验证书。
在external.xml的 profile 或网关里加:
<param name="p-early-media-support" value="true"/> <param name="tls-verify-policy" value="in"/>p-early-media-support设为true时,FreeSWITCH 会把上游的早期媒体透传给主叫,适合运营商回铃音必须透传的场景。tls-verify-policy的取值有in、out、all、none,我一般建议至少保持默认对入向校验,完全关闭校验只适合内网调试环境。这个参数在呼入呼出路由配置里虽然不直接参与号码匹配,但会影响呼叫是否能在 TLS 中继上正常建立,本质上是路由可达性的前置条件。
3.4 用 fs_cli 验证呼入命中的上下文
配好之后,验证呼入路由最直接的方法是看通道变量里的context和destination_number。
/usr/local/freeswitch/bin/fs_cli -x "show channels as delim ,"或者呼叫进行时在 fs_cli 里输入uuid dump <uuid>,观察输出里的context字段。如果显示的是public,说明入口配置没生效;如果显示from-pstn但没进 IVR,再回拨号方案里看正则是否覆盖了实际 DID。呼入路由配置的排查顺序永远是“入口 context → 匹配字段 → 执行动作”,不要一开始就怀疑正则。
4. 呼出路由配置:网关选择、号码变换与多中继选路
呼出路由比呼入路由更复杂,因为涉及网关选择、号码变换、主叫透传和多中继负载分担。很多生产故障不是呼不出去,而是号码变换错了,或者走了错误网关导致主叫号码被运营商拒绝。
4.1 分机呼出依赖 directory 里的 context
分机注册在 FreeSWITCH 上时,它所在的 directory 条目里也有context参数。这个 context 决定分机拨号时从哪张拨号方案开始匹配。常见的做法是把分机直接放到default,呼出规则也写在default里。
<user id="1001"> <params> <param name="password" value="1001"/> <param name="context" value="default"/> </params> </user>这里的关键是:分机呼出默认匹配的是default上下文,换句话说出局路由写在default里即可。如果你想让某个分机只能打内线,就把它的 context 改成internal-only,再在那个上下文里只放分机互拨的匹配规则。
4.2 出局号码变换:去前缀、加前缀和主叫改写
出局路由一般要做三件事:确认目标网关、变换被叫号码、决定透传的主叫号。看一个带号码变换的完整例子:
<extension name="outbound-trunk-a"> <condition field="destination_number" expression="^(0\d+)$"> <action application="set" data="effective_caller_id_number=${caller_id_number}"/> <action application="bridge" data="sofia/gateway/trunk-a/$1"/> </condition> </extension> <extension name="outbound-trunk-b"> <condition field="destination_number" expression="^(1[3-9]\d{9})$"> <action application="bridge" data="sofia/gateway/trunk-b/$1"/> </condition> </extension>第一个 extension 处理以0开头的国内长途,去0后交给trunk-a。第二个 extension 匹配13到19开头的 11 位手机号,原样交给trunk-b。这样一个分机拨号时,FreeSWITCH 会按号段自动选择中继,这是最朴素的按号段分流。
如果运营商要求主叫号码必须是专线固话,不能透传分机号,可以在 bridge 前改写主叫:
<action application="set" data="effective_caller_id_number=01088886666"/> <action application="set" data="caller_id_number=01088886666"/>effective_caller_id_number是 FreeSWITCH 外呼时实际放进 SIP 请求里的主叫号码,caller_id_number则是内部变量。只改前者更安全,因为内部计费和 CDR 还能保留原始分机号。
4.3 多网关选路与故障切换
只有一个网关的生产环境很少,多网关时选路逻辑就要考虑了。FreeSWITCH 的bridge支持按序尝试多个网关,也可以配合regex轮询方式做负载。先看按序尝试的写法:
<action application="bridge" data="sofia/gateway/trunk-a/$1|sofia/gateway/trunk-b/$1"/>|分隔符表示逐个尝试,前面的网关失败后自动尝试后面的。这种做法的优点是配置简单,缺点是如果第一个网关本身注册正常但路由不通,FreeSWITCH 要等到超时才会切第二个网关,中间会有明显延迟。
另一套做法是把选路逻辑写进 Lua 脚本里,适合网关状态要动态判断的场景:
local gw_list = {"trunk-a", "trunk-b", "trunk-c"} local dest = session:getVariable("destination_number") for _, gw in ipairs(gw_list) do if session:ready() then local ok, err = session:bridge("sofia/gateway/" .. gw .. "/" .. dest) if ok == true then return end end endLua 脚本方式的好处是能根据当前时间、网关注册状态、号码号段做任意逻辑组合。缺点是稍微增加了维护成本,但对网关数量超过三个的场景,它的可控性远好于在 XML 里堆|分隔符。
4.4 呼出路由和网络路由不要混为一谈
偶尔会有同事把 FreeSWITCH 的呼出选路和 OSPF 动态路由配置实验、交换机静态路由混在一起讨论。两者层级完全不同:OSPF 和静态路由解决的是 IP 层“下一跳给谁”的问题,FreeSWITCH 的呼出路由解决的是应用层“号码交给哪个中继”的问题。虽然都能叫“路由”,但排查思路完全不同。IP 层不通时,网关注册都会失败,先解决 SIP 注册,再谈号码选路,这个顺序不能反。
4.5 验证呼出是否走了预期网关
呼出路由排错比呼入多一个动作:看bridge实际拼接出的字符串。在 condition 里临时加日志,或者用 fs_cli 的dialplan debug直接看正则匹配过程。
/usr/local/freeswitch/bin/fs_cli -x "dialplan debug"开启后拨一次测试电话,控制台会打印每一步 condition 的匹配字段、正则和结果。确认正则匹配到哪个 extension、bridge里的$1替换成了什么。替换出错时经常是少写了一层括号导致$1取到的不是预期片段。dialplan debug 是呼出路由配置里最趁手的工具,比反复猜日志快得多。
5. 进阶技巧:用时间路由和 Lua 动态分发收掉长尾需求
呼入呼出的基础路由配置跑通之后,真正花时间的往往是一些只能“写死在逻辑里”的需求:工作时间外的呼入应该转语音信箱、特定号码段要走指定中继、不同客户前缀要落不同计费组。这些用 XML 也能做,但堆多了之后 extension 之间的关系靠肉眼很难维护,我自己通常会把它们收敛到 Lua 动态路由里。
5.1 时间路由用 schedule 还是 Lua
FreeSWITCH 有schedule和time-of-day相关的应用,但做时间路由最顺手的方式是直接读系统时间做分支。比如下面的 Lua 脚本放在呼入主 IVR 之前,判断当前时段是否在工作时间内:
local hour = tonumber(os.date("%H")) local is_work = (hour >= 9 and hour < 18) if is_work then session:transfer("main_menu", "XML", "default") else session:transfer("after_hours_vm", "XML", "default") end这段脚本用os.date拿到当前小时,再转移到不同的拨号方案 extension。transfer的三个参数跟 XML 里的transfer动作一致:目标 extension、dialplan 类型、目标 context。如果需求还要区分节假日,把节假日日期维护成一个表,在 Lua 里先查表再走分支,比在 XML 里一层层嵌套 condition 要清晰得多。
5.2 用通道变量把呼入呼出串起来
呼入路线和呼出路线在业务上常常是一条链:客服接到电话后需要外呼回访。这时有一个比较实用的技巧:在呼入路由里把原始主叫号码存成自定义通道变量,呼出时再取出来。这个变量能跨transfer保留,成为工单系统与话单系统关联的桥梁。
在from-pstn进入 IVR 的 extension 里加:
<action application="set" data="original_caller=${caller_id_number}"/>后续无论呼叫转到哪个 context 或脚本,original_caller都能在通道变量里读到。配合呼出路由里设置的effective_caller_id_number,就能同时保留“客户是谁”和“用哪个号码外呼”两个信息,日常回访和呼叫中心质检都会用到这个字段。
5.3 一个收尾习惯:命名统一,注释写清
FreeSWITCH 的 dialplan 是 XML,多人维护时命名稍不规范就会撞车。我自己的习惯是:context 名统一用from-和to-前缀区分方向,extension 名用“业务名-动作”格式,比如outbound-trunk-a、support_queue。加comment属性可以用在<extension>上,但内容不要太长,只写这个路由存在的理由即可。这个习惯不花时间,但后续做配置审计、对接计费系统时会省下大量沟通成本。每次改完配置,记得在 fs_cli 里执行reloadxml让拨号方案生效,再按前面讲的方法用 dialplan debug 做一次实测确认。
本文还有配套的精品资源,点击获取