FreeSWITCH呼入呼出路由配置实战:从XML dialplan到多网关选路
简介《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 内部从上到下逐条匹配 extensionextension 内部再逐条匹配 condition。任何一层匹配失败呼叫都会落进 context 默认的 no-match 处理通常就是拒绝或者放音。2.1 context 是路由的第一道闸门context 是 FreeSWITCH 里的逻辑容器可以理解成一张独立的拨号计划表。不同来源的呼叫被导向不同 context就是在做第一次路由分流。生产上我通常会拆出这几个 contextdefault分机互拨和出局、from-pstn外部呼入、from-did按中继或 DID 号码再细分、public只留匿名访问出口不放业务。看一个最小呼入 context 的 XMLcontext namefrom-pstn extension namemain-ivr condition fielddestination_number expression^(\d)$ action applicationtransfer datamain_menu XML default/ /condition /extension /context这里destination_number是 FreeSWITCH 内置的拨号变量表示被叫号码。正则^(\d)$匹配纯数字串能过滤掉一部分非法 URI。transfer动作把呼叫转到default上下文里名为main_menu的 extensionXML表示去 XML dialplan 里找。这样写的好处是呼入的逻辑统一收敛在from-pstn上下文后续要加黑名单、白名单、时间路由都往这个 context 里塞 extension 就行。2.2 extension 的匹配受 break 参数控制很多人以为 dialplan 是“匹配到就停”其实不完全是。FreeSWITCH 的匹配行为由break参数决定它在 extension 和 condition 两个层级都能出现。默认情况下一个 extension 内部所有 condition 都匹配成功且 action 执行完后路由就停了。但如果某个 condition 失败是否继续尝试下一个 extension要看 break 的取值。break 取值位置实际行为不写默认extension/condition匹配成功就停在当前 extensionon-falseextension当前 extension 有 condition 失败时继续尝试下一个 extensionon-trueextension当前 extension 匹配成功也继续尝试下一个 extensionneverextension强制只匹配当前 extension不往下走我一般只在需要“先试分机、不通再走溢出路由”的场景里用on-false。日常的呼入呼出路由配置不需要频繁改 break但你要知道它的存在否则排错时看到日志里“路由匹配成功却没有停止”会一头雾水。condition字段也能拆成多行多个 condition 之间默认是“与”的关系全部为真才执行 action。如果希望某个 condition 为假时走另一段逻辑用breakon-false加一个只有anti-action的 condition 是常见做法。2.3 先写一个能满足多数场景的拨号方案下面这段 XML 是生产环境里比较稳的起步配置按“分机互拨、出局去 0、外部呼入接 IVR”三个维度划分。为了少踩坑我把default和from-pstn分开写而不是揉在一个上下文里。context namedefault !-- 分机互拨分机号段 10xx -- extension nameinternal-extension condition fielddestination_number expression^(10[0-9]{2})$ action applicationbridge datauser/${destination_number}${domain_name}/ /condition /extension !-- 出局路由0 开头走主网关去掉前导 0 再转发 -- extension nameoutbound-local condition fielddestination_number expression^0(\d)$ action applicationbridge datasofia/gateway/gw-main/$1/ /condition /extension !-- 兜底找不到路由就放忙音 -- extension nameno-match condition fielddestination_number expression^.*$ action applicationplayback datatone_stream://%(400,200,480)/ /condition /extension /context context namefrom-pstn !-- 外部呼入统一先进 IVR -- extension namemain-ivr condition fielddestination_number expression^(\d)$ action applicationtransfer datamain_menu XML default/ /condition /extension /contextsofi/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 fielddestination_number expression^(\d)$ action applicationlog dataINFO 命中呼入路由被叫号码${destination_number}/ action applicationtransfer datamain_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.xmlprofile nameexternal param namecontext valuefrom-pstn/ param nameinbound-codec-prefs valuePCMU,PCMA,G722/ /profileSofia 网关的 context 在外呼网关配置里比如gw-main.xmlgateway namegw-main param nameusername valueroute-01/ param namepassword valuesecret/ param nameproxy value203.0.113.10/ param namecontext valuefrom-did/ /gateway两者的区别SIP profile 管的是“直接落在本机监听端口上的呼入”进哪个 contextSofia 网关管的是“上游通过该网关账号呼入”进哪个 context。很多时候分机呼出正常、外线呼入不对就是因为网关里没写 contextFreeSWITCH 用了默认的public。参数说明参数位置作用contextprofileSIP profile设置直接呼入到本端口的默认入口 contextcontextgatewaySofia 网关设置该中继呼入的入口 context优先级高于 profile 默认值inbound-codec-prefsSIP profile影响呼入侧协商的编码顺序G722 优先可改善语音质量3.2 呼入字段匹配用 DID 和主叫号段做二次分流进入from-pstn后多数场景还要做二次分流不同 DID 转不同 IVR、特定主叫号段直接转分机、黑名单拦截。这些都是在一个 context 里用多个 extension 实现匹配字段用destination_number和caller_id_number。context namefrom-pstn !-- 服务热线 DID 进客服队列 -- extension namedid-support condition fielddestination_number expression^4008888888$ action applicationtransfer datasupport_queue XML callcenter/ /condition /extension !-- 黑名单主叫直接挂断 -- extension nameblock-caller condition fieldcaller_id_number expression^13800138000$ action applicationhangup dataCALL_REJECTED/ /condition /extension !-- 其余 DID 走统一 IVR -- extension namedefault-ivr condition fielddestination_number expression^(\d)$ action applicationlog dataINFO 命中默认呼入路由DID${destination_number}/ action applicationtransfer datamain_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 namep-early-media-support valuetrue/ param nametls-verify-policy valuein/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 id1001 params param namepassword value1001/ param namecontext valuedefault/ /params /user这里的关键是分机呼出默认匹配的是default上下文换句话说出局路由写在default里即可。如果你想让某个分机只能打内线就把它的 context 改成internal-only再在那个上下文里只放分机互拨的匹配规则。4.2 出局号码变换去前缀、加前缀和主叫改写出局路由一般要做三件事确认目标网关、变换被叫号码、决定透传的主叫号。看一个带号码变换的完整例子extension nameoutbound-trunk-a condition fielddestination_number expression^(0\d)$ action applicationset dataeffective_caller_id_number${caller_id_number}/ action applicationbridge datasofia/gateway/trunk-a/$1/ /condition /extension extension nameoutbound-trunk-b condition fielddestination_number expression^(1[3-9]\d{9})$ action applicationbridge datasofia/gateway/trunk-b/$1/ /condition /extension第一个 extension 处理以0开头的国内长途去0后交给trunk-a。第二个 extension 匹配13到19开头的 11 位手机号原样交给trunk-b。这样一个分机拨号时FreeSWITCH 会按号段自动选择中继这是最朴素的按号段分流。如果运营商要求主叫号码必须是专线固话不能透传分机号可以在 bridge 前改写主叫action applicationset dataeffective_caller_id_number01088886666/ action applicationset datacaller_id_number01088886666/effective_caller_id_number是 FreeSWITCH 外呼时实际放进 SIP 请求里的主叫号码caller_id_number则是内部变量。只改前者更安全因为内部计费和 CDR 还能保留原始分机号。4.3 多网关选路与故障切换只有一个网关的生产环境很少多网关时选路逻辑就要考虑了。FreeSWITCH 的bridge支持按序尝试多个网关也可以配合regex轮询方式做负载。先看按序尝试的写法action applicationbridge datasofia/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 还是 LuaFreeSWITCH 有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 applicationset dataoriginal_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 做一次实测确认。本文还有配套的精品资源点击获取