简介:面向企业VoIP运维与开发人员的Freeswitch呼入呼出路由配置详解文档,聚焦呼入、外呼、SIP中继与拨号计划等关键环节,适合正在搭建基于Freeswitch与网关设备内呼外呼环境的读者。文档先梳理Freeswitch事件驱动架构与模块组成,再结合XML拨号计划讲解呼入路由转发、外呼对等中继模式及sip_profiles中继配置,并给出安全加密、负载均衡、错误处理、日志监控等落地建议。包体为1份doc文档,约221KB,目录按引言、项目背景、Core、Module等章节展开,包含系统启动过程、消息分发与MOD_SOFIA组成等模块化说明,可边读边对照实验环境验证。该文档已有6446人学习下载,可作为从零配置Freeswitch路由、理解拨号计划及排查SIP中继问题的实用参考资料;无论初次部署还是已有环境调优,都能从中获得清晰的配置思路。
1. 呼入呼出路由配置:先解决“电话进来没人接、出去就被挂断”的问题
电话打进来没人接,拨出去就被挂断,这是很多刚接触 FreeSWITCH 的人遇到的第一道坎。freeswitch 呼入呼出路由配置,说白了就是回答两个问题:外线来话该送到哪个分机,内部分机打外线该走哪条中继。路由没理清楚之前,软交换做得再顺手也没用。这篇笔记按呼入、呼出两条线拆拨号计划配置,给出可以直接抄进 XML 的最小路由,也会把最容易翻车的几个现场单独列出来。适合正在对接 SIP 中继、做分公司电话互通,或者想把拨号规则彻底理清的运维和通信工程师。
2. 先把路由骨架搭对:context、extension与condition怎么配合
2.1 三个概念管住所有入局与出局呼叫
FreeSWITCH 的呼入呼出路由,几乎全部在拨号计划(dialplan)里完成。拨号计划由 context、extension、condition 三层组成。context 是隔离域,一个呼叫进入某个 context 后,只在这里面找 extension,不会跑到别的 context 里捡到一条不相关的路由;extension 是一条完整的路由规则;condition 是规则里的匹配条件,常用 field 指定呼叫变量,用 expression 写正则表达式。
呼入和呼出在 FreeSWITCH 里没有本质区别,都是从 SIP 进入 dialplan 的呼叫,真正的差别是它们从哪个 context 进来。外部中继呼叫通过 external profile 接入,默认进入 public context;内部分机注册在 internal profile 上,拨号进 default context。所以常说的呼入路由,是处理“从中继进来的号码往哪送”;呼出路由,是处理“从分机拨出的号码怎么改、走哪个网关”。理解了这个入口差异,后面就不会把两个方向的规则搅在一起。
常见的部署是 x86 服务器,但 FreeSWITCH 也支持 arm 架构,树莓派、ARM 软路由上都有人跑;拨号计划语法完全一样,只是有些编译模块需要按平台单独确认。不管跑在哪,路由配置的骨架逻辑不变。
2.2 一条最小呼入路由:把外线电话接进分机
最常见的呼入需求:中继送来一个号码,希望转给某个内部分机。在 public context 或你自己指定的 context 文件里加一条 extension:
<context name="public_in"> <extension name="inbound_to_2001"> <condition field="destination_number" expression="^(2001)$"> <action application="bridge" data="user/2001@${domain_name}"/> </condition> </extension> </context>逻辑说明:condition 的 field 是 destination_number,也就是 SIP 请求里的被叫号码;expression 用 PCRE 正则,^ 和 $ 把匹配限定为整串,避免“2001”匹配成“20010”。action 里的 bridge 做真正的呼叫接续,data 用 user/分机号@域名的格式把呼叫送到内部分机。如果分机不在线,bridge 会失败,呼叫按失败原因继续执行后续动作,默认返回忙音。
参数说明:${domain_name} 是 FreeSWITCH 预置的全局变量,指向当前主机的域;如果给分机配了独立域,直接写那个域名。condition 里可以同时写多个 field,比如 caller_id_number 匹配主叫、destination_number 匹配被叫;匹配正则里的括号分组可以用 $1 引用到 action 中,这个特点在后面呼出号码清洗时很重要。
2.3 一条最小呼出路由:让分机能拨9出外线
呼出路由的最小形态是:分机拨 9 开头的号码,剥掉 9,从某个网关发出去。在 default context 里加 extension:
<context name="default"> <extension name="outbound_9"> <condition field="destination_number" expression="^9(\d+)$"> <action application="bridge" data="sofia/gateway/telecom/$1"/> </condition> </extension> </context>逻辑说明:分机拨 912345678 后,destination_number 是“912345678”,正则 ^9(\d+)$ 把 9 后面的数字捕获到 $1。bridge 的 data 用 sofia/gateway/网关名/$1,把去掉 9 后的号码送往名为 telecom 的网关。如果运营商要求被叫号码带 9 才能出局,data 里直接写 ${destination_number} 即可,具体按中继对接规范来定。
参数说明:网关名是 conf 里定义的 gateway 名称,不是 IP 地址。bridge 到网关前先做号码变换是呼出路由的常规操作,第 4 章会给完整写法。这里还有一个容易忽略的点:dialplan 从上往下匹配,多个 extension 有重合号码段时,先匹配到的先执行;想让某些条件继续往下走,可以在 condition 上写 break="never"。
2.4 路由匹配顺序:continue 与 break 的两个典型写法
dialplan 的顺序敏感是新手最容易忽略的。同一 context 里,两个 extension 都匹配同一个号码时,排在前面的先执行。如果你在 public context 里放了一个演示用的 echo extension,又往里加了呼入分配路由,很可能所有电话都进了回声测试而不是你的分机。
<extension name="record_all_in" continue="true"> <condition field="destination_number" expression="^(\d+)$"> <action application="set" data="call_record=true"/> </condition> </extension>逻辑说明:continue="true" 让这条 extension 执行完 set 后不中断匹配,继续寻找下一个 extension。这样可以在不改动原有呼入路由的前提下,给所有呼入打一个“需要录音”的标记。如果去掉 continue,record_all_in 就会吃掉所有呼叫,后面的路由永远轮不到。这是把公共逻辑放在路由头部时最典型的错误。
参数说明:extension 上的 continue 控制“整个 extension 执行完是否继续找下一条”;condition 上的 break="never" 控制“当前 condition 匹配完是否继续本 extension 内下一条 condition”。两者作用层级不同,别混用。实际项目中,我一般把录音、号码清洗这类公共逻辑放最前面并加 continue,把具体路由放后面;这样既不影响原有路由,又能给所有呼叫打标记。
3. 呼入路由配置:从外线中继到分机、IVR、语音信箱的实际写法
3.1 先定“电话进哪个门”:external profile与context的关系
呼入路由第一步不是写 extension,而是看呼叫从哪个 SIP profile 进来。FreeSWITCH 默认有两个 profile:internal 和 external。internal 用于内部分机注册,信号端口 5060;external 用于跟运营商或上游 SIP trunk 对接,端口 5080。每个 profile 里有一个 context 参数,决定该 profile 上收到的呼叫进入哪个 dialplan context:
<profile name="external"> <param name="context" value="public_in"/> <param name="sip-port" value="5080"/> </profile>逻辑说明:这段配置在 conf/sip_profiles/external.xml 里。把 context 从默认的 public 改成 public_in,相当于给所有从 external 进来的呼叫开了一条专用路由通道。内部注册的呼叫走 internal profile,进入 default context。呼入呼出要在逻辑上分家,第一步就是把这两个入口分开,否则后面所有匹配都会互相干扰。
参数说明:sip-port 是 external profile 的监听端口,很多运营商只允许固定端口对接,端口改了路由逻辑不变。我一般会建议新建一个 public_in context,而不是直接复用 public,因为 public context 默认带着 echo、MusicOnHold 等演示 extension,不清掉很容易误匹配。这里是纯配置调整,改完要重启 external profile 才会生效,这一点在第 5 章会单独展开。
3.2 呼入路由的三种去向:分机、IVR、语音信箱
下面是一段比较完整的呼入路由块,覆盖最常用的三种去向:转分机、转 IVR、转语音信箱。
<extension name="in_to_2001"> <condition field="destination_number" expression="^(2001)$"> <action application="bridge" data="user/2001@${domain_name}"/> </condition> </extension> <extension name="in_to_ivr"> <condition field="destination_number" expression="^(800\d{3})$"> <action application="answer"/> <action application="play_and_get_digits" data="2 8 3 7000 # /ivr/welcome.wav /ivr/choice.wav ivr_choice"/> <action application="transfer" data="${ivr_choice} XML default"/> </condition> </extension> <extension name="in_to_vm"> <condition field="destination_number" expression="^(5001)$"> <action application="answer"/> <action application="voicemail" data="default 5001"/> </condition> </extension>逻辑说明:第一条 bridge 到分机 2001;第二条 answer 后播放欢迎音并收按键,用户按键内容写入 ivr_choice,紧接着用 transfer 把 ivr_choice 作为新被叫号码转到 default context 执行;第三条直接进语音信箱。三条 extension 按号码段区分,互不干扰。
参数说明:play_and_get_digits 的参数顺序是“最少位数、最多位数、尝试次数、超时毫秒、结束符、提示音文件、无效输入音文件、结果变量”。很多人把变量名和文件名顺序写反,结果按键收集不到。这里 2 是最少位数,8 是最多位数,3 是尝试次数,7000 是超时毫秒,# 是结束符,ivr_choice 是结果变量。bridge 失败后的兜底可以在 bridge 后面直接加 voicemail:
<action application="bridge" data="user/2001@${domain_name}"/> <action application="voicemail" data="default 2001"/>这样分机没接起来时自动进留言,而不是干巴巴的忙音。
3.3 呼入路由的营业时间切换与号码归一化
呼入路由经常要跟着营业时间变:工作时间转 IVR,下班转语音信箱,节假日直接播报。FreeSWITCH 的 condition 原生支持时间匹配属性:
<extension name="business_hours_route"> <condition wday="1-5" hour="9-18"> <action application="transfer" data="ivr_main XML default"/> </condition> </extension> <extension name="after_hours_route"> <condition wday="6,0" hour="0-23"> <action application="voicemail" data="default 2001"/> </condition> </extension>逻辑说明:wday 的取值范围是 0-6,0 表示周日,所以周六周日写成 6,0。hour 9-18 表示 9 点到 18 点。第一条匹配时转到名为 ivr_main 的 IVR extension;不匹配就继续检查第二条。注意这两条 extension 的顺序:工作时间那条要放在前面,因为匹配成功后不会再往后走。
号码归一化是呼入路由里另一类高频需求。运营商送来的主叫可能带 +86 或 00,做黑名单、VIP 路由时直接比对经常对不上。用 regex 变量函数做清洗:
<action application="set" data="caller_clean=${regex(${caller_id_number}|^(?:\+?86|00)?(1[3-9]\d{9})$|$1)}"/>逻辑说明:set 会把 caller_id_number 里匹配到的分组内容存进 caller_clean,这样 +8613812345678、008613812345678、13812345678 三种格式都会被归一化成 13812345678。后续 condition 用 field="caller_clean" 再做黑名单或 VIP 判断,就避免了运营商格式差异带来的匹配失误。
4. 呼出路由配置:拨号前缀、号码变换与多网关调度的完整方案
4.1 呼出不改号,线路直接拒收
呼出与呼入本质上的差异,是呼出多了一个“号码变换”步骤。分机拨号往往是短号,比如 9+手机号、9+固话、或内线短号;但运营商中继要求的是标准 E.164 或特定号码格式。如果直接把分机拨的号码 bridge 到 gateway,常见后果是:9 没剥掉被运营商当拒收号码、手机号没补 0 被当作无效区号、固话没加区号被路由到别的城市。
所以呼出路由的骨架一定是:先匹配被叫号码 → 清洗/变换 → set 成新变量 → bridge 到网关。我把清洗和 bridge 分开写,中间过一遍日志,这样排查时能知道是哪一步改坏了。在正式配置之前,最好先用 fs_cli 看一条真实呼叫的 destination_number 到底长什么样,再写正则;不要凭想象写匹配,很多企业对短号、分机号段的规划有历史包袱。
4.2 可抄的呼出拨号计划:去9、补0、带上国家码
下面是一段可以直接放进 default context 的呼出配置,覆盖三种常见呼出类型:
<extension name="out_mobile"> <condition field="destination_number" expression="^9(1[3-9]\d{9})$"> <action application="set" data="outbound_number=$1"/> <action application="log" data="INFO inbound-number=${destination_number} outbound-number=${outbound_number}"/> <action application="bridge" data="sofia/gateway/telecom/${outbound_number}"/> </condition> </extension> <extension name="out_local"> <condition field="destination_number" expression="^9(0\d{2,3}\d{7,8})$"> <action application="set" data="outbound_number=$1"/> <action application="bridge" data="sofia/gateway/telecom/${outbound_number}"/> </condition> </extension> <extension name="out_international"> <condition field="destination_number" expression="^9(00\d{6,})$"> <action application="set" data="outbound_number=${destination_number:1}"/> <action application="bridge" data="sofia/gateway/international/${outbound_number}"/> </condition> </extension>逻辑说明:三条 extension 分别匹配 11 位手机号、带区号的固定电话、国际长途。手机号从 9 后面捕获 11 位,直接送给 telecom 网关;固话同理。国际长途那一条,用户拨 9+00+国家码+号码,去掉 9 后送 international 网关。${destination_number:1} 表示从第 1 个字符之后开始截取,也就是去掉开头的 9。
参数说明:set 把清洗后的号码存进 outbound_number,bridge 时用 ${outbound_number} 替换。中间加了一条 log,INFO 级别会在日志里打印最终发往网关的号码,出问题的时候第一个查这里。如果不想打印日志,删掉这一行即可。为什么不用 $1 直接写进 bridge?因为中间多了一个 set 变量之后,你可以在 bridge 前插入任何号码变换逻辑,也方便在 log 里看清清洗前后的差异。
4.3 多网关按优先级接续,跑一个不行换下一个
企业级呼出很少只有一个网关。可能是电信加联通双线路,也可能是主用运营商加备用运营商。FreeSWITCH 里做多网关最简单的方式是在同一个 bridge 里用竖线列出多个目的地:
<action application="bridge" data="sofia/gateway/telecom/${outbound_number}|sofia/gateway/unicom/${outbound_number}"/>逻辑说明:bridge 会从左到右依次尝试;第一个网关如果返回失败,比如网关未注册、对方无应答,会自动尝试第二个。整个过程对用户是透明的。但要注意,如果第一个网关“假成功”——SIP 返回 100 Trying 后迟迟没有 180 或 200,bridge 会一直等,直到底层超时才切到第二个。所以双网关策略要配合网关级的超时参数。
更精细的控制是先定义好候选网关列表,再用 loop 和 execute 逐条尝试,但大多数场景用 bridge 多目的地就能覆盖。网关对接时,SIP trunk 对端通常是一个固定 IP 加端口,不是动态注册,所以路由能否成功很大程度上取决于网关注册是否在线。检查网关在线状态用:
fs_cli -x "sofia status gateway telecom"如果所有网关都失败,可以在 bridge 后面加一段兜底,播放一个预录提示音再挂断:
<action application="playback" data="/var/record/outbound_fail.wav"/> <action application="hangup"/>这样就可以告诉用户“外线暂时不可用”,而不是干巴巴的忙音。
5. 呼入呼出路由配置常见问题排查:5个让你翻车的现场与解法
5.1 呼入能听到回铃但分机不响
现象:外线拨打后主叫侧正常听到回铃音,但内部分机没有任何反应,几秒后呼叫结束。
原因:这是典型的 bridge 已经执行、但内部分机不可达。可能分机没有注册,也可能 user/域名写错导致找不到该分机。另一个常见来源是正则把完整被叫号码截错,比如写成(200)匹配到 2001 的一部分,实际 bridge 给了不存在的号码。
解决:先确认分机是否注册,执行fs_cli -x "show registrations";再手工测试fs_cli -x "originate user/2001@127.0.0.1 &echo()"。如果这条能通,问题在路由匹配,检查 condition 正则和 context;如果这条也不通,检查分机配置和域。特别注意 external context 里是否有其它 extension 先匹配了 2001 并执行了错误动作。
5.2 呼出拨号后对方来电显示不对或直接拒收
现象:分机拨 9+号码,自己侧能听到接通,但对方来电显示不对,或者对方根本不响铃。
原因:最常见是没剥 9,把数字 9 当成被叫号码的一部分送给了网关。某些软交换或运营商收到带 9 的号码会拒收,或按短号处理。另一种是主叫号码没有透传,网关默认用了中继线路的主叫号码。
解决:先看 bridge 前的 outbound_number 是否已经是清理后的号码。我之前说过在呼出 extension 里加 log,这一步就是派这个用场。日志里确认号码没问题后,检查 gateway 配置里的 caller_id 相关参数,不同 FreeSWITCH 版本参数名略有差异,以你当前版本的示例配置为准。一般方向是让网关把主叫号码原样放进 SIP 请求。
5.3 呼入呼出混用default context导致号码互踩
现象:分机拨某个短号,比如 10086,结果触发的是呼入路由中的 IVR 或语音信箱,而不是出局路由。
原因:default context 同时承载了呼入和呼出。呼入电话从 external 进 default,呼出分机拨号也进 default;field 都是 destination_number,短号 10086 可能同时被呼入 extension 的号码段覆盖,两个业务逻辑就打架了。
解决:呼入独立 context,比如 public_in,只让 external profile 转发进去;呼出留在 default。然后在规划号码段时把分机号段和中继号段错开。分机用 2xxx,呼入特服用 8xxx,呼出统一 9 开头,这样互相踩的概率最低。这一步是路由规划层面的根治方案,比在正则里规避来得可靠。
5.4 正则匹配失败:destination_number末尾带#号
现象:SIP 话机拨号后,日志里 destination_number 显示为“2001#”或“+862001”,正则写^(2001)$死活匹配不上。
原因:很多话机和 IPPBX 在拨号结束时会带 # 表示结束;中继呼入时有些运营商把号码格式化成 +86 开头或 00 开头。destination_number 的实际值并不是你想象的那个“纯净号码”。
解决:在呼入 context 顶部加一条公共清洗,把非数字字符剥掉再走后续路由:
<extension name="normalize_in" continue="true"> <condition field="destination_number" expression="^(.+)$"> <action application="set" data="dest_clean=${regex(${destination_number}|[^0-9]||)}"/> </condition> </extension>逻辑说明:continue="true" 保证清洗执行完继续匹配下面的 extension。regex 把所有非数字字符替换成空串,dest_clean 就是纯数字。后续呼入 extension 的 condition 把 field 从 destination_number 改成 dest_clean。注意带号来源不同时,+86 前缀是否要保留要看业务,这里只做了去符号处理,不做国家码判断。
5.5 reloadxml不生效与Windows路径坑
现象:改了 xml,执行 reloadxml,再呼测试还是走老路由,甚至直接报 not found。
原因:FreeSWITCH 的 XML 配置是分层加载的。reloadxml 能重新加载大部分拨号计划,但 external profile 里的 context 参数和 gateway 参数改动,需要重启 SIP profile 才会生效。另一个常见原因是你在 Windows 版 FreeSWITCH 上改文件,路径分隔符、文件编码或权限问题导致 xml 没有真正被读到。
解决:只改 extension 内容时执行fs_cli -x "reloadxml";改了 profile 或 gateway 参数时执行fs_cli -x "sofia profile external restart"。注意 restart 会中断该 profile 上正在进行的呼叫,生产环境要挑窗口期。Windows 上优先用 UTF-8 无 BOM 保存,改完先确认文件能被正常解析,再进 fs_cli 验证。排查时用uuid debug抓一条呼叫看它实际加载了哪个文件,不要凭猜。
6. 验证路由配置的三板斧:originate、uuid debug与状态检查
6.1 用originate给路由做冒烟测试
改完路由不要急着让业务方拨号测试,先用 fs_cli 做冒烟:
fs_cli -x "originate user/2001@127.0.0.1 &echo()" fs_cli -x "originate sofia/gateway/telecom/13812345678 &echo()"逻辑说明:第一条命令模拟内部分机呼叫,验证 user 拨号串和分机注册状态;第二条命令直接从网关发起外呼,验证网关注册和号码变换结果。&echo() 是回声应用,接通后能听到自己的声音;如果直接呼叫真实号码,要确认测试对象能接受,避免打扰真实用户。
6.2 用uuid debug与状态检查定位问题
冒烟不通过时,先用状态检查缩小范围,再用 uuid debug 看细节:
fs_cli -x "show registrations" fs_cli -x "sofia status gateway telecom" fs_cli -x "show channels"呼叫发生时,用 show channels 拿到 uuid,再执行uuid debug <uuid>,日志里会逐条打印匹配过的 extension、condition 是否命中、action 执行顺序。这是排查路由配置的终极手段,比反复改 xml 重载高效得多。
我现在每次改完路由,都先跑一轮 originate 冒烟,再看一眼分机和网关的注册状态,最后才通知业务方测试。这个顺序养成了习惯之后,至少能拦掉一半“路由不生效”的反馈。希望帮到你。
本文还有配套的精品资源,点击获取