简介:一份面向 FreeSWITCH 运维与开发人员的呼入呼出路由配置详解,聚焦实际组网中拨号计划、SIP 中继和对等中继模式的落地方法。文档从 FreeSWITCH 核心架构讲起,逐步解析程序启动、消息分发、mod_sofia 模块,并结合 XML 拨号计划说明呼入转接、外呼经 SIP 中继出局等配置思路;附录还涵盖 TLS 加密、负载均衡、错误恢复和日志监控等生产环境注意事项。压缩包内仅含 1 个 doc 文档,包体约 221KB,虽体量不大,但目录结构清晰,便于按章节快速查阅。该资源已有 6448 人学习下载,适合需要系统理解 FreeSWITCH 路由机制并完成中继对接的中高级使用者。借助这份材料,读者能梳理清事件驱动架构与模块化设计,对照配置文件排查路由不通、中继注册失败等问题,并掌握对等中继模式下 IP、端口及认证参数的设置要点,减少线上调试验证成本。
1. 呼入呼出路由:FreeSWITCH 真正的"进"与"出"只有一张路由表
很多人第一次碰 FreeSWITCH,面对呼入呼出路由配置时最懵的不是命令不会敲,而是不知道一条呼叫在 FreeSWITCH 里到底走哪条链。呼入是运营商或 SIP 终端把 invite 送到你的机器,由 Dialplan 的 inbound 流程接住;呼出是你的话机、业务端要求系统把呼叫转交给 SIP gateway,再由 gateway 转发给运营商。表面上这是两个方向,实际只有一个路由核心——Dialplan。控制呼入的不在"呼入配置"里,控制呼出的也不在"呼出开关"里,它们都由同一张 route 表决定。把这条主线想清楚,你配的路由才能真正在话务高峰里立得住。
2. 呼入路由:外线号码怎么在 Dialplan 里找到归属
2.1 从 Sofia Profile 到 Context:第一道路由边界
呼入路由的第一步不是写 Dialplan,而是搞明白 invite 从哪个端口进来、由哪个 profile 处理,再进入哪个 context。FreeSWITCH 的 Sofia 模块就是 SIP 协议栈,它监听在某个 IP 和端口上,每个监听实例称为一个 profile。默认配置里 internal 监听 5060,external 监听 5080。internal 一般接内网话机注册,external 一般接运营商中继或外网 SIP 终端,这个分工在生产里必须固定,不然后面所有路由都跟着乱。
在 FreeSWITCH 的默认配置里,Sofia 处理完 invite 后会把呼叫交给 Dialplan,Dialplan 再根据 context 决定用哪一组 extension 来匹配。默认的 public context 是所有从外部进来的呼叫默认落点,default context 是内部分机的业务路由。如果你希望外部呼入先经过一道"安检"再进入内部业务,就要在 public 里放一条跳转规则,把呼叫 transfer 到 default context。
<extension name="public_to_internal"> <condition field="destination_number" expression="^(\d{3,12})$"> <action application="transfer" data="$1 XML default"/> </condition> </extension>这里 transfer 的 data 有三个部分:$1 是原样保留的被叫号码,XML 是指定用 XML Dialplan(而不是 lua 或 mod_dialplan_asterisk),default 是目标 context。逻辑说明:public 收到任意 3 到 12 位数字的被叫号码,就原封不动转交 internal 域内的 default context 继续匹配;如果号码不是纯数字(比如带了 * 或 #),这条规则不命中,呼叫会被 public 内的其他兜底规则处理或直接拒绝。
参数上有两个重点。condition 字段不只有 destination_number 可用,常用的还有 caller_id_number、network_addr、source 等。network_addr 配合正则可以做中继来源 IP 白名单,比如只允许运营商 SBC 的 IP 呼入,其他来源一律拒绝,这是防盗打的常见做法。另一个重点是 expression 里的起止锚定:写法上尽量以 ^ 开头、以 $ 结尾,否则号码前缀相同但位数不同的呼叫会误命中同一条规则。
2.2 condition 与 break:路由匹配的"投票表决"机制
呼入路由在 Dialplan 里的匹配方式是逐个 extension 尝试,看 condition 里的正则是否命中。一个 extension 里可以有多个 condition 和多个 action,执行顺序是从上到下。condition 的结果决定是否继续执行后续 action,break 字段则控制是否继续尝试下一个 condition 或下一个 extension,这个细节是呼入路由编排里最容易出歧义的地方。
<extension name="office_hours_main"> <condition field="destination_number" expression="^(6001|6002)$"> <action application="set" data="hangup_after_bridge=true"/> <action application="bridge" data="user/6001@${domain}"/> </condition> </extension> <extension name="night_voice"> <condition field="hour" expression="^(20|21|22|23|0)$" break="never"> <action application="playback" data="/usr/local/freeswitch/sounds/night_prompt.wav"/> <action application="hangup"/> </condition> </extension>上面这段业务含义是:上班时间打 6001、6002 直接转给坐席;晚上 8 点到凌晨 0 点之间来电统一播放提示音后挂断。第一个 extension 用 destination_number 匹配分机号;第二个 extension 先用 hour 字段判断时段,hour 是 FreeSWITCH 内置的时间变量,不需要查数据库,这是做呼入时段分流最轻量的做法。
参数说明上最值得关注的是 break。默认行为下,如果一个 condition 没命中,FreeSWITCH 会停止执行当前 extension、跳到下一个 extension 继续尝试;如果命中了,则继续执行当前 extension 里剩下的 condition 和 action。这里 break="never" 的作用是即使第一个 condition 不命中,也要继续尝试当前 extension 的后续 condition。把它用在"时段判断 + 号码判断"组合里,就能实现"只要在夜间时段,不管打哪个号码都进夜间语音"的效果。如果去掉 break="never",夜间来电且号码匹配 6001 时,FreeSWITCH 会停在第一个 extension 直接 bridge 给分机,你预期的夜间语音就永远不触发。
2.3 验证呼入路由是否命中:用 fs_cli 看 Dialplan 匹配过程
配完呼入路由不看匹配过程,等于闭着眼睛改配置。最常用的验证手段是 fs_cli 里的 dialplan trace。执行后,FreeSWITCH 会把当前呼入匹配了哪些 extension、哪些 condition 命中、执行了哪些 action 全部打印出来。
fs_cli -x "dialplan trace 6001@default"这条命令的作用是手动触发一次"虚拟呼叫",被叫号码 6001、使用的 context 是 default。输出里会列出每个 extension 的匹配结果,如果某个 condition 的正则写错了,这里会直接显示 "no match"。参数说明:6001 是你要测的号码,default 是 context 名,如果呼入走 public,则写成 13212345678@public。注意 dialplan trace 只是模拟匹配,不会真的呼叫分机或外线,所以可以放心在生产环境跑。
我习惯在每次改完呼入路由后,用 trace 把典型号码全部过一遍。比如测手机号 13800001111、测短号 6001、测带了 0 前缀的 01012345678,看它们分别命中哪条规则。命中不对就立刻回头改正则,比拿真话机一遍遍拨测成本低得多。
3. 呼出路由:把呼叫交给 gateway 才算真正"出局"
3.1 Gateway 配置:给外呼定义一个出口
呼出路由比呼入多一层,因为你要先定义"从哪出去"。outbound 的出口就是 SIP gateway。在 FreeSWITCH 里,gateway 可以理解为一条通往某个 SIP 中继服务器或运营商 SBC 的逻辑链路。它既决定了呼出请求发到哪个代理服务器,也决定了信令里带什么主叫号码和凭证。下面是一段最常用的单向呼出 gateway 配置,写在 conf/sip_profiles/external.xml 里。
<gateway name="trunk_cmcc_1"> <param name="proxy" value="sbc.cmcc.provider.example"/> <param name="register" value="false"/> <param name="username" value="01012340001"/> <param name="password" value="your_password"/> <param name="from-user" value="01012340001"/> <param name="from-domain" value="sbc.cmcc.provider.example"/> <param name="extension" value="01012340001"/> </gateway>这段配置的含义是定义一个名为 trunk_cmcc_1 的网关,所有发给它的呼叫都会送到 sbc.cmcc.provider.example。register=false 表示这个网关不做 SIP 注册,而是靠运营商侧把我们的来源 IP 加白,这是企业对接运营商中继最常见的模式。username 和 password 用于需要认证的场景,from-user 是信令里显示的呼叫方号码,from-domain 是 SIP 头里的域信息,运营商可能用它判断呼叫来源。
参数上要注意几件事。proxy 是必填的,写错单个字符整个网关不可用。register 如果设成 true,FreeSWITCH 会周期性地向运营商发送注册请求,遇到配置不当的运营商,可能引来鉴权失败日志刷屏。实际生产里,凡是运营商给了固定 IP 对接的,我一般都关掉注册,省掉注册周期带来的变量。from-user 和 extension 两处都写主叫号码,看着重复,但一个影响 SIP 信令里的 From 头,一个影响特定中继要求的 P-Asserted-Identity 之类字段,缺了哪个都可能触发运营商侧主叫校验失败。
3.2 外呼 Dialplan:拨号串里怎么把呼叫送上指定的网关
gateway 只是出口定义,真正让呼叫"走上这条路"的是 Dialplan 的外呼 extension。写法上最不容易出错的方式是在 bridge 的拨号串里直接指定 gateway 名。
<extension name="out_dial_provider"> <condition field="destination_number" expression="^(\d{11})$"> <action application="set" data="effective_caller_id_number=01012340001"/> <action application="bridge" data="sofia/gateway/trunk_cmcc_1/$1"/> </condition> </extension>这个 extension 的逻辑是:当话机或业务系统拨出的被叫号码是 11 位纯数字时,先把主叫号码强制设为 01012340001,再通过 sofia/gateway/trunk_cmcc_1 这个出口把 $1(被叫号码)送给运营商。这里 bridge 的 data 格式固定为 sofia/gateway/网关名/被叫号码,三层之间用斜杠分隔。
拨号串的参数里有一个高频坑:如果你在 bridge 里写成 sofia/gateway/trunk_cmcc_1/$0,$0 在 FreeSWITCH 里代表整个正则匹配的完整字符串,多数情况下结果和 $1 一样,但一旦正则里带了前导或后缀匹配,$0 就会带上多余字符,运营商那边看到的主叫或被叫可能被污染。另一个值得注意的点是 effective_caller_id_number 必须在 bridge 之前设置,这样才能在信令发送前改好主叫。实际业务里如果话务平台是通过 originate 命令发起外呼的,这个变量也要在 originate 的变量参数里预先设置,不能指望 Dialplan 里再补。
3.3 双网关与失败转接:不要指望 FreeSWITCH 自动负载均衡
生产环境里经常需要配两个运营商或两条中继,很多人以为配两个 gateway 后 FreeSWITCH 会自动做负载均衡或故障切换,这是呼出路由里最普遍的一个误解。标准 Dialplan 不支持对两个 gateway 做自动轮询或主备检测,你写哪条 bridge 它就送哪条。要实现双路由冗余,常见的做法是在 Dialplan 里先 bridge 主网关,失败后再次 bridge 备网关。
<extension name="out_dial_failover"> <condition field="destination_number" expression="^(\d{11})$"> <action application="set" data="effective_caller_id_number=01012340001"/> <action application="bridge" data="sofia/gateway/trunk_cmcc_1/$1"/> <action application="bridge" data="sofia/gateway/trunk_unicom_1/$1"/> </condition> </extension>这段配置的执行逻辑是:首选把呼叫送到 trunk_cmcc_1,如果这条 bridge 返回了非成功状态(比如超时、忙、拒绝),Dialplan 会继续执行下一条 action,也就是再尝试通过 trunk_unicom_1 送出。如果两条都不通,最后才会走到兜底处理。注意 bridge 是否算失败要取决于呼叫会话的最终状态,不是看网络通不通;网关能收到信令但被运营商拒绝(例如 403)也会被视为失败并触发下一条 action,这一点在实际运维里要清楚。
参数里没有额外的 failover 开关,真正的控制点在于"把多条 bridge 按顺序当成一个故障转移链"。这个方案不完美,比如没有心跳检测、不能感知中继质量劣化,但对大多数只需要做到"断线能自动切"的企业场景已经够用。
4. 号码规范化与路由优先级:规则多了不乱,靠的是归一化
4.1 呼入号码的归一化:正则加变量处理,别让格式卡住路由
呼入侧号码格式来自运营商时往往千奇百怪,有带 +86 的,有带 0 的,有自带区号不带 0 的。如果你不在路由入口把主叫号码归一化成同一格式,后面做时段路由、IVR 菜单、按主叫找客户时,识别逻辑会因为前后格式不一致而失灵。这种问法题最典型的表现是"同一个客户打进来,有时候能识别有时候不能"。
<extension name="normalize_caller_in"> <condition field="destination_number" expression="^(\d+)$"> <action application="set" data="norm_caller=${regex(${caller_id_number}|^\+?86|0|\+86)}"/> <action application="log" data="INFO normalized caller is ${norm_caller}"/> </condition> </extension>先解释正则参数:${caller_id_number} 取出原始主叫号码,正则 ^+?86|0|+86 会把开头的 +86、单独一个 0 全部替换为空字符串,这样 861381234567、+861381234567、01381234567 都会被归一化成 1381234567。set 执行完,norm_caller 这个变量就保存在当前呼叫的 channel 变量表里,后续同一次呼叫内的任何 extension 都能引用。
参数上的细节是,FreeSWITCH 的 regex 语法默认是替换所有命中的,所以上面这个写法能一次性处理掉"有没有 86、有没有 0"的三种情况。实际场景里,有的呼叫中心还要求保留区号的 0,那就把"^0(?=\d{3})" 这类规则拆开处理,不要一刀切。处理完务必留一条 log 输出,这个 log 在排障时能直接告诉你归一化是否达到预期。
4.2 呼出号码改写:用正则分组重排,别用字符串拼接堆逻辑
呼出方向则需要把用户拨的号改写成运营商要求的格式。比如用户习惯了拨 0+区号+号码,而运营商要求去 0 后加特定前缀。这里常见做法同样是把改写规则独立成一个 extension,用正则分组做重排,而不是在业务代码里写一堆 if else 拼字符串。
<extension name="rewrite_outbound_num"> <condition field="destination_number" expression="^0?(\d{3})(\d{8})$"> <action application="set" data="rewrite_num=0$1$2"/> <action application="bridge" data="sofia/gateway/trunk_unicom_1/${rewrite_num}"/> </condition> </extension>这个正则表达的意思是:用户拨的是 0+区号+8 位号码时,去掉 0,然后重新在开头补 0,最终保持原样;如果拨的本来就是区号开头没 0,则自动补 0。这里的 ${rewrite_num} 是刚 set 的变量,bridge 时直接在拨号串里引用它。逻辑上虽然补 0 这个动作"什么都没改",但它统一了两种输入格式的输出结果,让后续对端看到的始终是带 0 的区号格式。
参数说明上有一个关键点:正则里的 (?...) 分组必须从 $1 顺序编号,如果要改写的字段多,优先用捕获组而不是字符串函数,这样每段号码都被显式命名,后续改格式只需要动正则,不需要改 bridge 行。实际做外呼改号时,最怕的是在 bridge 里直接写死号码而忽略 rewrite_num 变量,一旦运营商要求调整格式,你就得每个业务 extension 挨个改,这种写法在排查时也难定位。
4.3 路由优先级的二层含义:先明白顺序,再谈优先级
路由优先级说白了就是 Dialplan 的执行顺序。这里有两层。第一层是同一个 context 内从上到下逐个 extension 匹配,先命中先处理;第二层是同一个 extension 内,执行完一个 action 后是否继续执行下一个 action。很多人在排障时只改 extension 顺序却不看 action 顺序,结果就是明明"优先级调了",行为没变。
需要注意的两个典型场景。场景一:public 里有多个呼入跳转规则,最宽松的规则别放在最前面,否则任何号码都先进这条,后面的规则永远轮不到。场景二:外呼里第一条 bridge 失败后,如果下一条不是 bridge 而是 playback,呼叫会被跳过外呼流程进入放音逻辑,这是配置错误,不是没设优先级。我的习惯是把"严格匹配的规则(指定号码、指定来源)"放在前,"宽泛兜底规则(任意号码)"放在最后,这样兜底永远不会吞掉精确路由。这个原则在呼入和呼出两侧都成立。
5. 呼入呼出路由配置避坑清单:现象、原因、解法
这一章全是在真实环境里踩过一遍的路由配置问题,每一条都是"现象 → 原因 → 解决"的完整链条,可以直接对着排查。
5.1 外线呼入听不到提示音,路由却没报错
现象:运营商号码打进来自动挂断,甚至在话机上连回铃都不完整;但 fs_cli 里看不到明显报错。
原因:呼叫进入的 context 与号码实际匹配的 extension 不在同一个 context。最常见的是内部 extension 写在 default 里,而呼入的 profile 把呼叫丢给了 public;public 里没有匹配规则,默认行为就是拒绝。另一种情形是 internal 和 external 的端口搞混,外部 invite 实际落在 5060(internal)上,而你所有路由和分机注册都在 external 的 5080 语境里。
解决:先用 fs_cli 执行 sofia status 看当前 profile 监听端口,确认外线实际进的是哪个 profile,再看对应 profile 的 dialplan 参数指向哪个 context。把呼入路由放进正确的 context,或在 public 里加一条 transfer 跳到 default。改完后用 dialplan trace 验证,这一步能直接看到匹配到了哪个 extension。
5.2 呼出被运营商拒接,日志里却有 200 OK
现象:外呼在 fs_cli 里能看到 200 OK,但电话就是没接通,运营商侧报"主叫号码未通过校验"。
原因:SIP 信令虽然送达了运营商,但信令里的主叫号码或主叫域不符合对方要求。多数情况是 effective_caller_id_number 没设置、或者 from-domain 和网关 proxy 不一致。
解决:在 bridge 前强制 set effective_caller_id_number 和 effective_caller_id_name,并确认 gateway 里的 from-user、from-domain 与运营商下发的数据一致。如果运营商要求主叫号码固定在某个白名单里,而业务侧传了其他号码,也会导致拒接,这种要从前端业务系统排查传参加。
5.3 改了 gateway 配置不生效,还是旧的
现象:修改了 gateway 的 proxy 或密码,重新拨号后发现信令仍然发往旧地址。
原因:gateway 配置是在 Sofia 模块加载时解析的,修改 XML 后不会自动热生效。部分环境里 reloadxml 只能让 Dialplan 生效,网关本身还需要重读 profile。
解决:先改 XML 文件,然后依次执行 reloadxml、sofia profile external rescan。如果 rescan 后仍不生效,就执行 sofia profile external restart,这一步会断开该 profile 上的所有在线话机,生产操作前务必确认在话务低谷期执行。Windows 环境下安装 FreeSWITCH 时路径和进程管理略有不同,但 fs_cli 里的 sofia 命令语法一致,照着执行即可。
5.4 外呼号码少了一位,正则匹配"吞号"
现象:用户拨 18612345678,实际到达运营商的号码变成 1861234567,或者对端回 404。
原因:正则表达式里使用了未锚定的 \d+,而号码后面跟着的结束符、业务前缀被贪婪匹配吞进去了。比如表达式 ^(\d+)\d$ 的 \d+ 会尽量多吞,剩余末尾一位被吞掉。
解决:统一在表达式末尾加 $,把匹配范围锚定在完整号码上。如果要匹配带后缀的分机拨打场景,就把后缀单独用非捕获组写出,不要放进 \d+ 的贪婪范围里。改完正则后,至少拨一个带前缀、一个带后缀、一个纯净号码的用例做回归。
5.5 呼入正常、话机互打正常,但转分机时不振铃
现象:外部呼入后路由成功执行到 bridge,但分机不振铃,日志显示 user 不可达。
原因:bridge 时用了 user/6001@${domain},而当前呼叫里 ${domain} 的值与用户话机注册的域名不一致。外线呼入的 domain 往往来自运营商请求头里的 Host 字段,和内部注册域名不同,导致 FreeSWITCH 在自己的注册表里找不到该 user。
解决:在呼入 extension 最前面显式 set domain=你的注册域名,或者在 bridge 前用 export 把 domain 固定下来。不要依赖动态的 ${domain} 来做内部桥接,除非你清楚知道外线请求头里的 domain 就是注册域。这个坑在双域名、多租户环境里尤其常见,建议统一用 export 变量隔离。
6. 用拨测脚本让路由配置变成可回归的操作
路由配置这块,光靠"改完拿真话机拨一通"是不够的,因为你没法保证下一次改动不会破坏上一次的成果。我习惯把路由验证做成一组可重复执行的 fs_cli 脚本,每次改完配置至少跑一遍。
fs_cli -x "originate user/6001@default &bridge(sofia/gateway/trunk_cmcc_1/13800138000)"这条命令的含义是:从内部分机 6001 发起一路呼叫,直接通过 trunk_cmcc_1 网关呼到 13800138000。它等效于一次拨号操作,但不依赖任何外部话机,几十秒内就能确认网关路由是否正常。执行后看返回的 SIP 状态码,如果返回 200 OK 说明呼叫已经送达运营商侧;如果返回 603 或超时,则可能是主叫校验、号码格式或网关认证问题。
另一个高频命令是查看路由匹配明细:
fs_cli -x "dialplan trace 13800138000@default"trace 结果里会列出每一步匹配的 extension 名和 action,正则有没有命中、有没有走到兜底,一目了然。我的个人习惯是每改一次路由 XML,先 reloadxml,再跑三遍:一遍测手机号、一遍测带 0 区号、一遍测未知主叫(caller_id_number 为空)。三次全过再提交版本。这套习惯帮我挡掉了至少七八成因为正则边界和号码格式问题引发的路由故障,也省掉了在话机上反复拨测的时间。呼入呼出路由配置说到底就是把"入口收敛、出口规范"这两件事做扎实:外部呼入只留一条受控的跳转通道,外呼号码改写和网关选择各自独立成规则。沿着这个思路改配置,后面加中继、调呼入策略时不会手忙脚乱。希望帮到你。
本文还有配套的精品资源,点击获取