FreeSWITCH 呼入呼出路由机制:拨号计划与 SIP 中继查表逻辑详解
简介:这份文档面向使用 Freeswitch 搭建 VoIP 通信平台的运维与开发人员,聚焦呼入呼出路由配置这一关键环节,帮助读者理清拨号计划、SIP 中继与对等中继模式下的外呼实现思路。资源包内共 1 个 doc 文件,约 221KB,内容围绕 Freeswitch 整体架构展开,涵盖程序启动流程、消息分发机制、mod_sofia 模块组成与启动过程等核心知识点,并配有术语定义与条目编号规范,便于按模块查阅。文档以个人学习总结的形式组织,从 CORE 到 MODULE 逐层梳理,适合作为理解 Freeswitch 内部运行机制与路由配置逻辑的参考笔记。目前已有 6455 人学习下载,对于需要排查呼入呼出路由问题、补充架构认知的初中级读者具有一定参考价值。
1. 一通电话进来,FreeSWITCH 到底先看哪张表
很多人第一次配 FreeSWITCH 呼入呼出,都会卡在同一个地方:SIP 注册成功了,软电话也显示在线,可拨出去没声音、打进来直接挂断。翻日志只看到一堆CHANNEL_CREATE、CHANNEL_HANGUP,像看天书。问题往往不在 SIP 层,而在路由层——FreeSWITCH 收到 INVITE 之后,真正决定这通电话命运的是拨号计划(Dialplan)和 SIP 中继(Gateway)这两张表,它们谁先被查、按什么顺序匹配、匹配失败后走哪条兜底路径,才是呼入呼出能不能通的底层机制。搞懂这套路由查表逻辑,比背十条配置命令有用得多,因为后面所有踩坑几乎都能回溯到「哪张表没命中」上。
2. 呼入路由:INVITE 落地后拨号计划怎么匹配
2.1 从 SIP Profile 到 Dialplan 的交接点
外部呼叫进入 FreeSWITCH,第一站不是拨号计划,而是mod_sofia加载的 SIP Profile。Profile 在conf/sip_profiles/下定义,它决定了监听哪个 IP、哪个端口、用 UDP 还是 TCP/TLS,以及收到 INVITE 后把呼叫交给哪个 context。这里有个容易被忽略的参数叫context,默认值通常是public。也就是说,从外部中继进来的呼叫,默认落在public这个拨号计划上下文里,而不是你平时写内部分机规则的default。
常见的实现思路是:public上下文只做一件事——识别来源,然后把呼叫转进default或直接送到某个具体 extension。如果你把内部分机规则全写在default里,却忘了在public里做transfer,外部呼叫就会在public里找不到匹配 extension,直接HANGUP。这就是「注册正常但打进来挂断」最常见的原因,日志里表现为public上下文下No route。
Profile 里还有dialplan参数,指定用哪个拨号计划模块解析,默认是XML。这意味着呼叫进入后,FreeSWITCH 会去conf/dialplan/下找对应 context 的 XML 文件。public.xml和default.xml就是两个最核心的路由表。
2.2 XML Dialplan 的匹配顺序与 extension 结构
拨号计划的核心单位是extension,每个 extension 有name和一组condition。匹配逻辑是:按文件内 extension 出现的顺序逐条尝试,第一个所有 condition 都满足的 extension 被选中执行。condition 里最常用的是destination_number,用正则匹配被叫号码。
<!-- conf/dialplan/public.xml 片段:外部呼入先识别来源再转内部 --> <extension name="from_trunk_to_internal"> <!-- 匹配来自中继的呼叫,网关名在 sofia 的 gateway 配置里定义 --> <condition field="destination_number" expression="^(\d{4})$"> <!-- 把呼叫转进 default 上下文,交给内部分机规则处理 --> <action application="transfer" data="$1 XML default"/> </condition> </extension>这段配置的含义是:当被叫号码是 4 位数字时,把呼叫transfer到default上下文的同名 extension。$1是正则捕获组,代表那 4 位号码。transfer的第三个参数XML指定拨号计划类型,第四个default是目标 context。少了这个 transfer,外部呼叫就停在public里出不去。
匹配顺序这件事值得多说一句。FreeSWITCH 不是「最精确匹配优先」,而是「文件顺序优先」。如果你把一条宽泛的正则写在前面,比如^(\d+)$,它会把后面所有更具体的规则全部吃掉。血泪经验是:具体规则往前放,兜底规则放最后。很多人调路由调半天,最后发现是 extension 顺序写反了。
2.3 呼入路由的完整链路与日志验证点
把链路串起来看:INVITE 到达 →mod_sofia按 Profile 的context把呼叫放进public→ XML 拨号计划按顺序匹配public.xml→ 命中后执行transfer进default→default.xml再按顺序匹配内部分机规则 → 命中后bridge到目标分机或网关。
验证这条链路是否走通,看日志里的几个关键事件就够了。CHANNEL_CREATE出现说明呼叫已建立通道;紧接着应该看到CHANNEL_PROGRESS或CHANNEL_ANSWER;如果CHANNEL_CREATE之后直接CHANNEL_HANGUP,基本就是拨号计划没命中。更细的排查可以开sofia global siptrace on,看 INVITE 的To头和实际被叫号码是否一致——有时候号码被网关加了前缀,正则自然匹配不上。
提示:
public和default两个 context 的职责要分清,public管「从哪来」,default管「到哪去」。混在一起写,后期加中继必然翻车。
3. 外呼路由:Gateway 与 bridge 怎么把呼叫送出去
3.1 SIP 中继(Gateway)的注册与对等模式
外呼的前提是有一个能出去的 SIP 中继。FreeSWITCH 里中继叫 Gateway,配置在conf/sip_profiles/external/下的 XML 文件里。摘要里提到的「对等中继模式」,指的是不向对方注册、直接按对端地址发 INVITE 的模式,对应配置里register设为false。
<!-- conf/sip_profiles/external/peer_trunk.xml:对等中继,不注册 --> <gateway name="peer_trunk"> <!-- 对端网关地址,实际部署时替换为网关设备 IP --> <param name="proxy" value="192.0.2.10"/> <!-- 对等模式不向对端注册 --> <param name="register" value="false"/> <!-- 主叫号码,部分网关要求必须携带 --> <param name="caller-id-in-from" value="true"/> </gateway>proxy是对端地址,register=false表示对等模式。这里的关键机制是:Gateway 本身不发起呼叫,它只是bridge应用的一个目标描述。真正发起外呼的是bridge应用,格式是sofia/gateway/peer_trunk/被叫号码。sofia是端点模块名,gateway是固定关键字,peer_trunk是 Gateway 名,后面跟号码。
对等模式和注册模式的区别在于信令方向。注册模式下 FreeSWITCH 主动向对端发 REGISTER 维持在线状态,对端知道你的地址;对等模式下双方都直接按配置的地址发 INVITE,省掉了注册开销,但要求双方地址固定可达。这也是为什么摘要里强调「SIP 中继就是填写我的 FreeSWITCH 的 IP 和端口」——对等模式必须让对端知道往哪发。
3.2 bridge 应用的执行时机与失败分支
外呼路由写在default.xml里,典型结构是一条匹配特定前缀的 extension,命中后执行bridge。
<!-- conf/dialplan/default.xml 片段:9 开头走对等中继外呼 --> <extension name="outbound_via_trunk"> <!-- 9 开头加 8 位号码,捕获实际被叫 --> <condition field="destination_number" expression="^9(\d{8})$"> <!-- 先设置主叫显示,再 bridge 到中继 --> <action application="set" data="effective_caller_id_number=1000"/> <action application="bridge" data="sofia/gateway/peer_trunk/$1"/> </condition> </extension>set在bridge之前执行,设置主叫号码,因为部分网关会校验主叫格式。bridge的data里$1是去掉前缀 9 之后的 8 位号码。这里有个设计取舍:前缀剥离放在正则捕获里做,而不是用strip应用,好处是一条正则同时完成匹配和号码提取,少一次字符串操作,坏处是正则写错时号码会带着前缀发出去,对端直接拒呼。
bridge失败时会走bridge的失败分支。默认行为是播放忙音或直接挂断,但更稳妥的做法是在bridge后面加hangup_after_bridge=true,并在bridge前用set配置continue_on_fail,让失败后能落到备用路由。常见的实现思路是配两条 extension:第一条走主中继,continue_on_fail=true;第二条走备用中继。这样主中继不可达时不会直接挂死。
3.3 外呼链路的日志与抓包定位
外呼不通时,日志里最先看的是bridge那一行有没有报错。如果看到Cannot create outgoing channel of type [sofia],说明 Gateway 名写错或没加载;如果看到INCOMPATIBLE_DESTINATION,多半是编解码不匹配,需要在 Gateway 或 Profile 里显式指定codec-prefs。抓包层面,sofia global siptrace on能看到发出的 INVITE 里Request-URI是不是对端期望的格式,From头里的主叫号码对不对。
还有一个隐蔽的坑:对等模式下如果对端要求认证,而你没配username/password,INVITE 会收到 401/407,FreeSWITCH 不会自动重发带认证的 INVITE,呼叫直接失败。日志里表现为SIP/2.0 407 Proxy Authentication Required,但 FreeSWITCH 侧只显示 bridge 失败,不点开 siptrace 根本看不到 407。这就是为什么外呼问题一定要开信令跟踪。
4. 路由配置避坑:五条踩出来的排查记录
4.1 呼入直接挂断,日志只有 CHANNEL_CREATE 和 HANGUP
现象:外部呼叫进来,软电话不响,日志里CHANNEL_CREATE后紧跟CHANNEL_HANGUP,没有任何CHANNEL_PROGRESS。
原因:Profile 的context指向public,但public.xml里没有匹配该被叫号码的 extension,或者匹配到了但没有transfer出去,呼叫在public里无路可走。
解决:先确认 Profile 的context值,再检查public.xml里是否有覆盖该号码段的正则。临时验证可以在public.xml加一条^(\d+)$的兜底 extension,直接transfer到default,看呼叫是否能进内部分机。能进就说明是public规则缺失,再逐步收紧正则。
4.2 外呼提示无法接通,siptrace 里看到 407
现象:拨外线后听忙音或直接失败,bridge日志没有明显错误,开 siptrace 才看到对端回407 Proxy Authentication Required。
原因:对等中继配置里没填username和password,但对端网关要求认证。FreeSWITCH 收到 407 后不会自动带认证重发,bridge 直接失败。
解决:在 Gateway 配置里补上username和password参数,值由对端提供。补完后重启mod_sofia或sofia profile external restart让配置生效。注意对等模式下即使register=false,认证参数依然要填,因为 INVITE 本身可能被要求认证。
4.3 内部分机互拨正常,外呼号码带前缀发出去被拒
现象:拨 9 加号码,对端拒呼,抓包发现Request-URI里号码还带着 9。
原因:正则捕获组写错,或者bridge的data里直接用了destination_number而不是$1。destination_number是完整被叫,包含前缀。
解决:确认bridge的data用的是捕获组$1而非${destination_number}。如果号码需要做更复杂的变换,用set配合regex应用先处理,再bridge,不要在一个正则里塞太多逻辑。
4.4 呼入能通但单通,一方听不到声音
现象:呼叫建立成功,但只有一方能听到声音,或者双方都无声。
原因:媒体路径问题,常见于 NAT 环境或 Profile 的ext-rtp-ip/ext-sip-ip没配对外网地址。信令通了但 SDP 里通告的媒体地址不可达,RTP 包发不到对端。
解决:在 Profile 里显式配置ext-rtp-ip和ext-sip-ip为对外可达的地址,内网环境配local-network-acl让 FreeSWITCH 正确区分内外网。验证方法是抓 RTP 包看有没有双向流,只有单向流基本就是 SDP 地址通告错了。
4.5 改了 XML 不生效,重启也没用
现象:修改default.xml后reloadxml,新规则不生效,甚至重启 FreeSWITCH 后依旧走旧逻辑。
原因:XML 语法错误导致整个文件解析失败,FreeSWITCH 回退到上一次成功解析的缓存,日志里会有Error Parsing XML但容易被忽略。另一种情况是改错了文件——conf/dialplan/default/目录下的子文件也会被加载,规则可能写在子文件里而不是default.xml。
解决:reloadxml后立刻看日志有没有解析错误,用xml_locate dialplan命令确认当前生效的规则内容。改配置前先xmllint校验语法,能省掉大量「改了没反应」的玄学时间。
5. 路由设计的取舍:为什么 FreeSWITCH 把匹配权交给文件顺序
FreeSWITCH 的拨号计划选择「文件顺序优先」而不是「最长匹配优先」,这个设计取舍值得单独想一层。传统电话交换机的路由表是按前缀长度做最长匹配的,规则之间互不干扰;FreeSWITCH 反过来,把匹配权完全交给配置文件的书写顺序。表面看这是把复杂度推给了使用者,实际上换来的是可预测性——你看到的第一条命中的规则就是实际执行的规则,不需要在脑子里跑一遍匹配算法。代价是规则顺序成了隐式契约,加规则时必须想清楚插在哪,插错位置就会静默覆盖后面的逻辑。
这个取舍在呼入呼出场景里体现得特别明显。呼入侧public上下文通常规则少、职责单一,顺序问题不突出;外呼侧default上下文规则多,前缀、号码段、兜底路由混在一起,顺序一乱就是「明明配了却不走」。我一般会这么理解:FreeSWITCH 把拨号计划当成一个有序的决策链,而不是一张无序的查找表,所以配置时应该按「最具体 → 最宽泛 → 兜底」三段式组织,每段内部再按业务优先级排。这样即使后面加规则,也知道该往哪一段插。
验证顺序是否符合预期,有个笨但有效的办法:在每条 extension 的condition里临时加log应用,打印命中的 extension 名,然后打几通测试电话看日志顺序。比对着 XML 空想靠谱得多。另一个习惯是给兜底规则单独放文件末尾,并加注释标明「以下为兜底,勿在其后插入规则」,防止后来者(包括三个月后的自己)插错位置。
还有一个进阶方向是把路由逻辑从 XML 里抽出来,用mod_lua或mod_python写拨号计划。脚本化拨号计划的好处是可以用代码做条件判断、查数据库、调外部接口,不受 XML 顺序约束;坏处是每次呼叫都要执行脚本,性能开销比纯 XML 匹配高。呼叫量不大的场景,脚本化带来的可维护性提升远大于性能损失;高并发场景则要谨慎,XML 匹配在核心层是 C 实现的,快得多。这个取舍没有标准答案,取决于你的呼叫模型是「规则稳定、量大」还是「规则多变、量小」。
从那以后我每次动拨号计划,都强制走一遍「改 XML → xmllint 校验 → reloadxml → 看解析日志 → 打测试电话看 CHANNEL 事件序列」的流程,不再靠重启碰运气。路由这东西,玄学都是没看日志造成的。希望帮到你。
本文还有配套的精品资源,点击获取