FreeSWITCH呼入呼出路由配置详解:从Dialplan到Gateway的实战指南

发布时间:2026/10/24 16:52:46
FreeSWITCH呼入呼出路由配置详解:从Dialplan到Gateway的实战指南

简介:这份文档面向使用 Freeswitch 搭建 VoIP 通信平台的运维与开发人员,聚焦呼入呼出路由配置这一关键环节,帮助读者理清拨号计划、SIP 中继与对等中继模式下的外呼实现思路。资源包内共 1 个 doc 文件,约 221KB,内容围绕 Freeswitch 整体架构展开,涵盖程序启动流程、消息分发机制、mod_sofia 模块组成与启动过程等核心知识点,并配有术语定义与条目编号规范,便于按模块查阅。文档以个人学习总结的形式梳理了系统架构与模块协作关系,可作为理解呼入路由、外呼策略及中继配置的参考底稿,适合需要快速建立 Freeswitch 整体认知的初中级读者对照实践。目前已有 6455 人学习下载,具备一定的参考热度。

1. 从一通打不通的外线说起:FreeSWITCH 呼入呼出路由到底在配什么

很多团队第一次把 FreeSWITCH 接上 SIP 网关,内部分机互拨一切正常,一拨外线就静默挂断,或者外面打进来直接落到默认的public上下文里被拒。问题往往不在网关,而在拨号计划(Dialplan)和 SIP 中继(gateway)这两层没对齐。这份 FreeSWITCH 呼入呼出路由配置详解,核心就是围绕dialplan的 XML 路由规则、sip_profiles下的中继定义,以及呼入落到哪个 context、呼出走哪条 gateway 这条主线展开的。它适合已经能把 FreeSWITCH 跑起来、但卡在“内呼通、外呼不通”这一步的运维和通信开发人员,也适合需要给小型呼叫中心做落地路由的工程师。下面按“先看清结构、再动手配、最后排坑”的顺序拆。

2. 先看清 FreeSWITCH 的呼叫骨架:Core、Module 与 Dialplan 的分工

在改任何一行 XML 之前,得先知道一个呼叫进来之后,FreeSWITCH 内部是谁在接、谁在转、谁在决定下一步。这份文档把 Core 启动、模块加载、事件分发讲得比较细,落到路由配置上,真正要盯住的是三个东西:SIP 端点模块(mod_sofia)、拨号计划模块(mod_dialplan_xml)、以及中继网关(gateway)。它们的分工决定了你改哪个文件、重启哪个模块。

2.1 从 switch.c 启动到模块加载:路由能力从哪来

FreeSWITCH 主入口在src/switch.c的main(),它做四件事:解析启动参数、给全局目录变量SWITCH_GLOBAL_dirs赋值、创建 pid 文件、调用switch_core_init_and_modload()。这个函数内部先跑switch_core_init()做核心初始化,再调switch_loadable_module_init()加载模块。模块加载顺序很关键:先加载集成模块CORE_SOFTTIMER_MODULE、CORE_PCM_MODULE,然后遍历conf/modules.conf和conf/post_load_modules.conf,最后为每个模块的 runtime 入口起线程。

对路由来说,mod_sofia必须在modules.conf里被加载,否则 SIP 栈根本不存在,sip_profiles里的中继配置也不会生效。常见做法是确认modules.conf里有<load module="mod_sofia"/>,并且mod_dialplan_xml也在列表里——后者负责把dialplan/目录下的 XML 解析成可执行的路由动作。如果只加载了 sofia 没加载 dialplan_xml,呼叫能进来但找不到路由,表现就是直接挂断。

提示:改完modules.conf后不要只reloadxml,模块增删需要重启进程或至少reload mod_sofia,否则新模块的接口不会注册到 hash 表里。

2.2 事件分发与呼叫状态:为什么路由要看 CHANNEL_CREATE 和 CHANNEL_ANSWER

文档里列了一长串事件类型,路由配置真正相关的是SWITCH_EVENT_CHANNEL_CREATE、SWITCH_EVENT_CHANNEL_ANSWER、SWITCH_EVENT_CHANNEL_BRIDGE、SWITCH_EVENT_CHANNEL_HANGUP。事件分发机制是:switch_event_init()初始化EVENT_DISPATCH_QUEUE队列并起分发线程,队列容量和 CPU 个数相关,消息先 push 进队列再由switch_event_dispatch_thread分发。

这跟路由有什么关系?当一通外线呼入,mod_sofia收到 INVITE 后创建 channel,触发CHANNEL_CREATE,然后进入拨号计划匹配。如果你在 dialplan 里配了bridge到某个 gateway,成功桥接时会有CHANNEL_BRIDGE;如果 gateway 认证失败或对端拒绝,你会看到CHANNEL_HANGUP且 cause 是CALL_REJECTED或NORMAL_TEMPORARY_FAILURE。排障时用fs_cli订阅这些事件,比盲改 XML 快得多:

# 订阅与路由相关的核心事件,观察一通外呼的完整生命周期 fs_cli -x "/event plain CHANNEL_CREATE CHANNEL_ANSWER CHANNEL_BRIDGE CHANNEL_HANGUP"

这条命令的逻辑是让fs_cli以 plain 格式订阅四类事件,输出里能看到每个 channel 的Caller-Destination-Number、variable_sip_gateway、Hangup-Cause。参数说明:plain是输出格式,后面跟事件名列表,多个事件用空格分隔。实际排障时我一般再加CHANNEL_EXECUTE,能看到 dialplan 执行到哪个 application 就断了。

2.3 Dialplan 与 SIP Profile 的边界:谁决定呼入 context,谁决定呼出 gateway

这是最容易混淆的一层。呼入方向,sip_profiles/internal.xml或external.xml里的<param name="context" value="public"/>决定这通呼叫落到哪个 dialplan context;呼出方向,dialplan 里的<action application="bridge" data="sofia/gateway/网关名/号码"/>决定走哪条中继。也就是说,profile 管“进门”,dialplan 管“出门”,gateway 定义在sip_profiles/gateway/或 profile 内的<gateway>段里。

文档里提到 mod_sofia 由 FreeSWITCH 的 TU 层加 Nokia sofia-sip 协议栈组成,NUA 对上、NTA 对下,传输层 UDP/TCP 对应用层透明。落到配置上,这意味着你在 gateway 里写register还是register=false、proxy指向谁,决定了 NUA 的行为;而dialplan只关心“把呼叫交给哪个 gateway 名字”。两者通过 gateway 名字解耦,改中继地址不用动 dialplan,这是这套结构最大的好处。

3. 呼入路由落地:从 external profile 到 public context 的完整链路

呼入不通,九成问题出在“呼叫进来了但没匹配到正确的 context 或 extension”。这一章按配置文件顺序走一遍:先确认 external profile 的 context 和端口,再在 public context 里写 extension 匹配被叫号码,最后用transfer或bridge落到内部分机或 IVR。

3.1 external profile 的关键参数:context、sip_port 与 auth-calls

外部呼入一般走conf/sip_profiles/external.xml。核心参数就几个:sip_port是监听端口,网关对接常用 5060;context决定呼入落到哪个 dialplan,默认是public;auth-calls控制是否对呼入做认证,对接网关时如果网关不做注册,通常设false,否则会被 401 挡掉。下面是一段精简后的 profile 片段:

<profile name="external"> <settings> <!-- 外部呼入监听端口,与网关侧填写的一致 --> <param name="sip-port" value="5060"/> <!-- 呼入落到 public context,由 dialplan/public.xml 接管 --> <param name="context" value="public"/> <!-- 网关不注册时关闭呼入认证,避免 401 --> <param name="auth-calls" value="false"/> <!-- 允许的编解码,按网关实际能力裁剪 --> <param name="inbound-codec-prefetch" value="PCMA,PCMU"/> </settings> </profile>

逻辑说明:sip-port必须和网关侧配置的对端端口一致,网关填 5060 你这边就不能改成 5080;context写public后,所有从这条 profile 进来的呼叫都会去dialplan/public.xml找 extension;auth-calls设false的前提是网关侧做了 IP 白名单或专线对接,公网暴露的 profile 不要这么设。参数怎么改:如果网关要求注册,把auth-calls改回true并在directory里建对应用户;编解码按网关实际支持的填,填了网关不支持的会导致协商失败、单通或无声。

3.2 public context 的 extension 匹配:destination_number 正则与 transfer 动作

呼入落到public后,dialplan/public.xml里的 extension 按destination_number正则匹配。常见做法是匹配网关送来的被叫号码前缀,然后transfer到内部 context 的分机,或者直接bridge到某个分机。下面这段匹配以0开头的外线号码并转到内部defaultcontext:

<include> <context name="public"> <!-- 匹配网关送来的任意被叫号码,按需收窄正则 --> <extension name="from_gateway"> <condition field="destination_number" expression="^(\d+)$"> <!-- 转到内部 default context,由内部 dialplan 继续路由 --> <action application="transfer" data="$1 XML default"/> </condition> </extension> </context> </include>

逻辑说明:expression="^(\d+)$"捕获全部数字被叫号码,$1在transfer里引用捕获组;transfer的data格式是“目标号码 XML context”,这里把号码原样带到defaultcontext 再匹配分机。参数怎么改:如果网关送来的是带区号的号码,正则改成^0(\d{10})$之类,$1取后十位;如果呼入要直接进 IVR,把transfer换成answer加playback或ivr应用。失败时看什么:fs_cli里dialplan没匹配会打No route,检查destination_number实际值和正则是否对得上,用fs_cli -x "show channels"看dest字段。

3.3 呼入直转分机与 IVR 的分流写法

实际项目里往往不是所有呼入都转分机,总机号进 IVR、直线号直转分机。做法是在publiccontext 里按号码段写多个 extension,靠正则区分。比如^9(\d{3})$进 IVR,^8(\d{3})$直转分机:

<extension name="to_ivr"> <condition field="destination_number" expression="^9(\d{3})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="ivr" data="main_ivr"/> </condition> </extension> <extension name="to_extension"> <condition field="destination_number" expression="^8(\d{3})$"> <action application="transfer" data="$1 XML default"/> </condition> </extension>

逻辑说明:answer先应答避免主叫听到静默,sleep 500给媒体协商留时间,ivr指向conf/ivr_menus/下的菜单名;直转分支用transfer把后三位带到defaultcontext。参数说明:sleep单位毫秒,设太大主叫会觉得慢,500 到 1000 之间比较稳;ivr的菜单名要和ivr.conf.xml里的name一致,写错会报Cannot find IVR。这里的分流逻辑完全靠正则顺序,FreeSWITCH 按文件内 extension 顺序匹配,第一个命中的生效,所以把更具体的正则放前面。

4. 呼出路由落地:gateway 定义、bridge 写法与对等中继模式

外呼不通的典型现象是拨号后立刻挂断,fs_cli里看到sofia/gateway/xxx返回404或503。这一章从中继定义开始,到 dialplan 里的bridge写法,再到对等中继模式下 IP 和端口的填法,把外呼链路补齐。

4.1 gateway 定义:register、proxy 与对等中继的填法

中继定义可以放在conf/sip_profiles/external/下的独立 XML,也可以写在 profile 内的<gateway>段。对等中继模式(不注册、按 IP 对接)的关键是register设false,proxy填网关地址和端口。下面是一个对等中继的 gateway 定义:

<gateway name="gw_pstn"> <!-- 对等中继不向网关注册 --> <param name="register" value="false"/> <!-- 网关地址和端口,按实际对接填写 --> <param name="proxy" value="192.0.2.10:5060"/> <!-- 主叫号码前缀,部分网关要求带区号 --> <param name="caller-id-in-from" value="true"/> <!-- 认证用户名密码,对等模式网关不校验时可留空 --> <param name="username" value=""/> <param name="password" value=""/> </gateway>

逻辑说明:register=false表示 FreeSWITCH 不主动向网关发 REGISTER,网关侧按 IP 信任;proxy是外呼 INVITE 的目的地址,写错就是503;caller-id-in-from=true让主叫号码放在 From 头,部分网关只认 From 不认 P-Asserted-Identity。参数怎么改:网关要求注册时把register改true并填username/password;proxy支持host:port,端口不写默认 5060;如果网关要求特定 realm,加realm参数。失败时看什么:fs_cli -x "sofia status gateway gw_pstn"看 State 是否NOREG(对等模式正常)或REGED,fs_cli -x "sofia loglevel all 9"开详细日志看 INVITE 发到哪。

4.2 dialplan 外呼 extension:bridge 到 gateway 的号码拼接

外呼 extension 一般放在defaultcontext,匹配内部拨号习惯,比如0开头走外线。核心动作是bridge到sofia/gateway/网关名/号码,号码部分要按网关要求补全或去前缀:

<extension name="outbound_pstn"> <!-- 内部拨 0 加号码走外线 --> <condition field="destination_number" expression="^0(\d{7,11})$"> <!-- 去掉前缀 0,把剩余号码交给网关 --> <action application="set" data="effective_caller_id_number=1001"/> <action application="bridge" data="sofia/gateway/gw_pstn/$1"/> </condition> </extension>

逻辑说明:^0(\d{7,11})$匹配 0 加 7 到 11 位号码,$1是去掉 0 后的号码;effective_caller_id_number设置外显号码,不设会用分机号,部分网关会拒;bridge的data格式是sofia/gateway/网关名/被叫号码,网关名必须和 gateway 定义里的name完全一致。参数怎么改:网关要求带区号就在$1前拼区号,比如sofia/gateway/gw_pstn/010$1;需要走多条中继做负载就写多个 gateway 名并用|分隔,FreeSWITCH 会按顺序尝试。失败时看什么:Hangup-Cause是CALL_REJECTED多半是网关拒了主叫号码或号码格式,NO_ROUTE_DESTINATION是 gateway 名写错或没加载。

4.3 外呼失败的分层排查:从 gateway 状态到 SIP 响应码

外呼排障我一般按三层走:先看 gateway 状态,再看 dialplan 是否命中,最后看 SIP 响应码。第一层fs_cli -x "sofia status"确认gw_pstn在列表里且 State 正常;第二层fs_cli -x "console loglevel debug"后拨号,看日志里有没有Dialplan: outbound_pstn这行,没有就是正则没匹配;第三层看 INVITE 的响应,403是网关策略拒、404是号码不存在、503是网关不可达、488是编解码协商失败。

# 三层排查的组合命令 fs_cli -x "sofia status" # 第一层:gateway 是否加载 fs_cli -x "console loglevel debug" # 第二层:开 debug 看 dialplan 命中 fs_cli -x "sofia loglevel all 9" # 第三层:看 SIP 信令细节

逻辑说明:sofia status列出所有 profile 和 gateway 的状态;console loglevel debug让 dialplan 匹配过程打到控制台;sofia loglevel all 9把 sofia-sip 协议栈日志开到最细,能看到完整 INVITE 和响应。参数说明:日志级别排完记得调回info和0,否则日志量会拖慢系统。这套顺序的好处是先排除配置层再进协议层,避免一上来就抓包。

5. 避坑与排查:呼入呼出配置里最容易翻车的五件事

这一章是我自己踩过、也见别人反复踩的五个坑,每个按“现象 → 原因 → 解决”写。路由配置的玄学大多不是 FreeSWITCH 本身的问题,而是 profile、dialplan、gateway 三者的边界没对齐。

5.1 呼入直接挂断,日志显示 No route

现象:外线打进来,主叫听到一声忙音或直接断,fs_cli里出现No route to destination。原因:呼入落到的 context 里没有匹配destination_number的 extension,或者 profile 的context写成了default但default.xml里没有对应正则。解决:先用fs_cli -x "show channels"看呼入 channel 的context和dest实际值,再对照dialplan/public.xml的正则;常见错误是网关送来的号码带+或空格,正则^(\d+)$匹配不上,改成^\+?(\d+)$并做号码规整。

5.2 外呼 401/403,网关拒绝认证

现象:拨外线后立刻断,SIP 响应401 Unauthorized或403 Forbidden。原因:gateway 的register和username/password与网关侧不一致,或者对等中继模式下网关仍要求认证。解决:fs_cli -x "sofia status gateway gw_pstn"看注册状态,401说明认证失败,核对username/password和网关侧账号;对等模式确认网关侧是否真的关了认证,没关就补认证参数或让网关侧加 IP 白名单。

5.3 单通:能听到对方,对方听不到自己

现象:外呼接通后一方有声一方无声,或呼入后主叫听不到 IVR。原因:媒体路径没打通,常见是ext-rtp-ip/ext-sip-ip没设或设错,NAT 环境下 RTP 包发到了内网地址。解决:在 profile 里显式设ext-rtp-ip和ext-sip-ip为公网地址,fs_cli -x "sofia status profile external"确认ext-rtp-ip生效;抓包看 RTP 是否双向,单向就是地址填错。

5.4 改完 XML 不生效,reloadxml 也没用

现象:改了dialplan/public.xml或 gateway 定义,reloadxml后行为不变。原因:reloadxml只重载 XML 注册表,gateway 定义变更需要sofia profile external rescan或重启 profile;模块增删更要重启进程。解决:dialplan 改动用reloadxml即可,gateway 改动用fs_cli -x "sofia profile external rescan",profile 参数改动用sofia profile external restart,模块改动重启进程。

5.5 多网关负载时呼叫总走第一条

现象:配了两条 gateway 做冗余,但所有外呼都走第一条,第一条挂了也不切。原因:bridge的data里多个 gateway 用|分隔时,FreeSWITCH 按顺序尝试,但第一条返回503后是否继续取决于失败原因和continue_on_fail设置。解决:在bridge前设continue_on_fail=true,或改用distributor模块做真正的负载;简单冗余可以在bridge的 data 里写sofia/gateway/gw1/$1|sofia/gateway/gw2/$1并确认失败码会触发切换。

6. 进阶:用 dialplan 变量和事件钩子把路由做成可观测的

配置能通只是第一步,真正上线后要能回答“这通电话为什么走了这条路由”。我的习惯是在 dialplan 里埋变量、在事件里留痕,把黑匣子变成可查的链路。具体做法是在 extension 里用set打标记,再用fs_cli或 ESL 按变量过滤。

<extension name="outbound_pstn_tagged"> <condition field="destination_number" expression="^0(\d{7,11})$"> <!-- 打路由标记,便于事后按变量检索 --> <action application="set" data="route_tag=pstn_gw1"/> <action application="set" data="effective_caller_id_number=1001"/> <action application="bridge" data="sofia/gateway/gw_pstn/$1"/> </condition> </extension>

逻辑说明:route_tag是自定义 channel 变量,会随CHANNEL_CREATE等事件带出,事后用fs_cli -x "show channels"或 CDR 里的variable_route_tag就能知道这通走了哪条规则。参数说明:变量名不要和系统保留变量冲突,前缀加个业务名比较稳;如果要多级路由,可以在每个bridge前改route_tag的值,最后一条生效的就是实际走的那条。

验证方法上,我一般做两件事:一是用originate主动发起测试呼叫,不依赖外部网关,先验证 dialplan 匹配和变量设置;二是订阅CHANNEL_HANGUP事件,把Hangup-Cause和route_tag一起打到日志,跑一轮回归就能看出哪条路由失败率高。

# 主动发起测试呼叫,验证 dialplan 匹配和变量 fs_cli -x "originate {route_tag=test,origination_caller_id_number=1001}sofia/gateway/gw_pstn/013800000000 &park()" # 订阅挂断事件,按 route_tag 过滤失败 fs_cli -x "/event plain CHANNEL_HANGUP"

逻辑说明:originate的{}里是 channel 变量,&park()让呼叫停在 park 状态便于观察,不真正接通;第二条订阅挂断事件,输出里看variable_route_tag和Hangup-Cause的对应关系。参数说明:origination_caller_id_number设主叫号码,测试时用测试号避免骚扰真实用户;park()可以换成&playback( silence_stream://2000 )做短时媒体测试。

有个血泪教训:早期我图省事,所有外呼都写在一个 extension 里,正则写得很宽,结果某次网关调整号码格式后,一半外呼匹配到了错误分支,排查花了大半天。从那以后我每次改 dialplan 都强制走一遍originate测试加事件订阅,确认route_tag和Hangup-Cause对得上才上线。这套习惯比事后抓包省事得多,希望帮到你。

本文还有配套的精品资源,点击获取