
简介这份文档面向使用 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 namefrom_trunk_to_internal !-- 匹配来自中继的呼叫网关名在 sofia 的 gateway 配置里定义 -- condition fielddestination_number expression^(\d{4})$ !-- 把呼叫转进 default 上下文交给内部分机规则处理 -- action applicationtransfer 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 namepeer_trunk !-- 对端网关地址实际部署时替换为网关设备 IP -- param nameproxy value192.0.2.10/ !-- 对等模式不向对端注册 -- param nameregister valuefalse/ !-- 主叫号码部分网关要求必须携带 -- param namecaller-id-in-from valuetrue/ /gatewayproxy是对端地址registerfalse表示对等模式。这里的关键机制是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 nameoutbound_via_trunk !-- 9 开头加 8 位号码捕获实际被叫 -- condition fielddestination_number expression^9(\d{8})$ !-- 先设置主叫显示再 bridge 到中继 -- action applicationset dataeffective_caller_id_number1000/ action applicationbridge datasofia/gateway/peer_trunk/$1/ /condition /extensionset在bridge之前执行设置主叫号码因为部分网关会校验主叫格式。bridge的data里$1是去掉前缀 9 之后的 8 位号码。这里有个设计取舍前缀剥离放在正则捕获里做而不是用strip应用好处是一条正则同时完成匹配和号码提取少一次字符串操作坏处是正则写错时号码会带着前缀发出去对端直接拒呼。bridge失败时会走bridge的失败分支。默认行为是播放忙音或直接挂断但更稳妥的做法是在bridge后面加hangup_after_bridgetrue并在bridge前用set配置continue_on_fail让失败后能落到备用路由。常见的实现思路是配两条 extension第一条走主中继continue_on_failtrue第二条走备用中继。这样主中继不可达时不会直接挂死。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/passwordINVITE 会收到 401/407FreeSWITCH 不会自动重发带认证的 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让配置生效。注意对等模式下即使registerfalse认证参数依然要填因为 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 事件序列」的流程不再靠重启碰运气。路由这东西玄学都是没看日志造成的。希望帮到你。本文还有配套的精品资源点击获取