☰
FreeSWITCH呼入呼出路由配置:Dialplan与SIP Profile联动详解
2026/9/30 11:34:15 网站建设 项目流程

简介:呼入呼出路由配置详解文档,面向使用FreeSWITCH搭建VoIP通信系统的运维、集成与二次开发人员。文档系统梳理了FreeSWITCH V1.2.7的核心架构与路由机制,围绕呼入、呼出两条主线,详细说明如何通过XML拨号计划处理外部来电,以及如何以对等中继模式配置SIP中继并填写FreeSWITCH的IP与监听端口,实现稳定外呼。同时,作者结合与网关设备的实际对接经验,给出了TLS安全加密、多中继负载均衡、错误恢复与备用路由、日志与性能监控等生产环境配置建议。压缩包内共1个文件,为约221KB的Doc文档,内容涵盖FreeSWITCH整体架构、程序启动与消息分发、核心模块介绍、MOD_SOFIA模块组成与启动等章节,目录清晰,便于循序阅读。已有6448人学习下载,对于需要独立完成呼入呼出路由规划、中继对接及故障排查的工程师,是一份务实且可直接参考的说明文档。

1. 呼入呼出路由配置:一张 Dialplan 吃下全部呼叫方向

做 FreeSWITCH 路由配置,最常见的一个误解是呼入、呼出要分开两套逻辑去写。实际拆过之后你会发现,呼入和呼出最终都汇到同一张拨号计划(Dialplan)里,区别只在入口:呼入走 sofia profile 的 context,呼出走 originate 时指定的 dialplan 或在 dialplan 里通过 bridge/forward 触发。这套配置的核心是三件事:vars.xml 里的全局变量、sip_profiles 里的中继网关、dialplan 里的路由规则。只要把这三件套的对应关系理清,内呼外呼、SIP 中继、对等中继模式基本都能稳稳落地。本文按我实际配置 FreeSWITCH V1.2.7 内部测试环境的思路来拆,适合正被路由绕晕的人直接照着改。

2. 配置三件套:vars 变量、SIP Profile 和 Dialplan 的联动关系

2.1 vars.xml:全局变量决定路由基线

刚接触 FreeSWITCH 的人往往先改 dialplan,改了半天发现号码匹配不上,最后查出来是全局变量的锅。vars.xml 里定义的变量作用域覆盖所有 context,dialplan 里的${local_ip_v4}、${domain}都是从这儿取的。我们测试环境里最常用的默认值是:

<X-PRE-PROCESS cmd="set" data="domain=$${local_ip_v4}"/> <X-PRE-PROCESS cmd="set" data="local_ip_v4=192.168.1.100"/> <X-PRE-PROCESS cmd="set" data="dialplan=XML"/> <X-PRE-PROCESS cmd="set" data="default_areacode=010"/>

<X-PRE-PROCESS>不是运行时解析,而是 FreeSWITCH 加载 vars.xml 时做的预处理,相当于先执行这些赋值,后面的 XML 配置里所有${local_ip_v4}都会被替换成192.168.1.100。如果你在 conf 目录下已有默认 vars.xml,需要重点检查domain和local_ip_v4这两个值,外呼网关的注册或对等中继常因为这里写成了默认的localhost或127.0.0.1,导致对端注册过来时域名匹配不上。

2.2 sip_profiles:呼入的入口和呼出的出口

sip_profiles 决定 FreeSWITCH 监听哪些 IP 和端口,以及每个 profile 默认把呼叫丢到哪个 context。很多路由不生效,不是 dialplan 写错,而是呼入的包根本没进你预期的 context。内部测试环境我一般开一个 internal profile 用于内部分机注册,再开一个 external profile 对接网关或上游 SIP 中继:

<profile name="external"> <settings> <param name="sip-ip" value="192.168.1.100"/> <param name="sip-port" value="5080"/> <param name="dialplan" value="XML"/> <param name="context" value="public"/> <param name="inbound-codec-prefs" value="PCMU,PCMA"/> <param name="outbound-codec-prefs" value="PCMU,PCMA"/> <param name="rtp-ip" value="192.168.1.100"/> </settings> </profile>

这个 profile 监听 5080 端口,收到的呼入全部落到publiccontext。对等中继模式下,网关侧把 SIP 中继的地址填成192.168.1.100:5080,FreeSWITCH 这边不用注册账号,只要外部 profile 允许匿名呼入(accept-blind-reg等参数按需开),包就能进来。需要提醒的是顺带检查external的auth-calls参数,对等中继场景通常设成false,否则网关发来的 INVITE 会被当成未认证请求拒绝。

2.3 dialplan 才是路由本体

路由规则全写在conf/dialplan/下的 XML 文件里。FreeSWITCH 按 context 组织 dialplan,同一张表里既处理分机互拨,也处理走中继的外呼。关键是理解匹配顺序:当一个呼入到达publiccontext,它会按<extension>的先后顺序逐条尝试匹配。<condition>里的表达式命中后执行<action>。一个最简单的外呼到 PSTN 网关:

<extension name="outbound_pstn"> <condition field="destination_number" expression="^9(\d{7,})$"> <action application="bridge" data="sofia/gateway/gsm_gateway/$1"/> </condition> </extension>

destination_number是叫到 FreeSWITCH 的号码,正则^9(\d{7,})$匹配以 9 开头的号码,截掉 9 后把剩余部分桥接到名为gsm_gateway的网关。bridge 的执行逻辑是创建一条新呼叫到网关,接通后将两条腿桥在一起。条件的背后是 FreeSWITCH 对每个呼叫建立 channel 后,把呼叫的元数据填进模板,再做条件求值,命中才执行动作。任何字段都能做条件,destination_number和caller_id_number用得最多。

2.4 网关 gateway 和对等中继的关系

外呼的出口在网关配置里。很多没做过 VoIP 的人分不清 SIP Profile 和 Gateway 的区别:Profile 是 FreeSWITCH 自己的监听口和属性,Gateway 是 FreeSWITCH 主动连接的上游设备的账号或地址。对等中继模式下,网关侧没有账号体系,FreeSWITCH 配置里同样可以不填username/password,只填地址:

<gateway name="gsm_gateway"> <param name="proxy" value="192.168.1.200"/> <param name="register" value="false"/> <param name="extension-in-contact" value="true"/> </gateway>

register=false意味着 FreeSWITCH 不做 REGISTER 注册,直接发 INVITE 到 proxy 地址。extension-in-contact让 FreeSWITCH 在 Contact 头带上主叫号码,部分网关没有这一项会回 486 或直接拒绝匿名呼叫。这个配置跟你对接的网关设备强相关,网关要求注册模式就填register=true并补username/password,要求对等中继就把register关掉。

3. 呼入路由:从 SIP 包进来到分机响铃的完整链路

3.1 呼入时数字怎么被解析和匹配

呼入链路从外部 INVITE 到达 profile 开始。sofia 模块解析 SIP 报文后,把 Request-URI 里的号码存入 channel 的destination_number,把主叫号码存入caller_id_number,随后进入你在 profile 里指定的 context。很多人上来就写<condition field="destination_number" expression="^(100[0-9])$">,但发现分机没反应,原因往往是号码带了前缀或参数。

调试呼入匹配最直接的手段是在 dialplan 里打印变量。我用得最多的是<action application="log" data="INFO dest=${destination_number} caller=${caller_id_number}"/>,然后到控制台看日志。V1.2.7 的日志格式比较简单,信息里能看到完整的被叫号码。判断号码到底长什么样,比反复改正则猜要快得多。

呼入分发常见做法是按被叫号码做分支,打电话到总机 0 转 ivr,打某个号码段直接转分机:

<extension name="ivr_main"> <condition field="destination_number" expression="^0$"> <action application="answer"/> <action application="playback" data="ivr/ivr_main.wav"/> </condition> </extension> <extension name="ext_2000_2005"> <condition field="destination_number" expression="^(200[0-5])$"> <action application="bridge" data="user/$1@${domain}"/> </condition> </extension>

第一个 extension 匹配到 0,answer 后播放 IVR 语音;第二个 extension 匹配2000-2005,直接桥到本地分机。user/$1@${domain}是 FreeSWITCH 的内部呼叫语法,$1是正则捕获的完整号码。需要留意的是bridge和transfer的区别:transfer会重新进入 dialplan 匹配新的 context 或 extension,而bridge只是呼叫另一条腿,通道建立后还会继续在原来的 dialplan 上下文里。

3.2 呼入默认路由和失败兜底

呼入一个没有匹配到任何 extension 的号码时,FreeSWITCH 会返回 404。对生产环境来说这不够友好,通常会在 context 末尾加一条默认路由。兜底的方式有两种:一种是转到语音邮箱,一种是转到前台座席组:

<extension name="default_inbound"> <condition> <action application="answer"/> <action application="playback" data="tone_stream://4000"/> <action application="hangup"/> </condition> </extension>

没有 field 的 condition 等于无条件命中。这里 answer 后播放 4 秒提示音再挂断,至少让主叫明确听到已被接起的声音。有的环境希望做总机人工接听,就把 playback 换成bridge到固定坐席分机。要注意兜底 extension 不要写在前面,否则所有号码都被它截胡。

3.3 同一个号码段呼入与呼出走向不同

实际环境中常见的需求是:同一个号码段,从外部呼入落地到 A 组坐席,从内部分机呼出时却要交给网关处理。这类场景不要试图在 context 名称上玩花样,而是用条件字段区分来源。呼入的 channel 的sofia_profile和network_addr可以用来判断来源:

<extension name="inbound_from_gateway"> <condition field="sofia_profile" expression="^external$"/> <condition field="destination_number" expression="^(6000)$"> <action application="bridge" data="user/6001@${domain}"/> </condition> </extension>

同时命中两个 condition 才执行 action。这比维护两张 dialplan 表清晰得多。需要提醒的是 condition 之间是 AND 关系,不是 OR,想表达"号码是 6000 或 6001"要拆成两个 extension 或者用正则^(6000|6001)$。

4. 外呼路由:对等中继模式下的网关对接与 originate 外呼

4.1 外呼的三种触发方式

FreeSWITCH 外呼不是只有拨号计划这一条路。实际拆项目时会碰到三种触发方式:分机拨号触发、ESL 或 API 直接 originate、呼叫被 transfer 到外呼 extension。第一种最常见,分机拨 9 开头号码,dialplan bridge 到网关;第二种多用于 CRM 点击外呼或自动外呼系统,通过originate命令拉起呼叫;第三种是呼叫先在本地被接听,再由 IVR 流程转出去。

对等中继模式下,分机拨号的外呼模板延续第 2 章的写法,核心动作是 bridge。但要注意,外部网关未必接受任意主叫号码,Freedom 这类网关可能要求主叫必须在白名单内。常见做法是 bridge 前先 set 主叫号码:

<extension name="out_gsm"> <condition field="destination_number" expression="^9(\d+)$"> <action application="set" data="effective_caller_id_number=10086"/> <action application="bridge" data="sofia/gateway/gsm_gateway/$1"/> </condition> </extension>

effective_caller_id_number只影响当前呼叫向对端显示的主叫号,不影响本地分机的号码。需要注意 bridge 到网关时sofia/gateway/网关名/号码的语义:FreeSWITCH 会在该网关的 proxy 地址上发起一条呼出,号码作为 Request-URI 传过去。

4.2 originate 拉起外呼和 inline dialplan

CRM 或呼叫中心场景通常不走分机拨号,而是由平台侧直接呼出。ESL 命令的典型写法:

originate user/2000@${domain} 9${destination} &bridge(sofia/gateway/gsm_gateway/${destination})

这条命令的执行逻辑是这样的:先让分机 2000 响铃(主叫腿),分机接听后再由&bridge()生成一条新呼叫到网关。&后跟的是拨号计划应用,不是立即执行,而是等主叫腿进入应答状态后才触发。这样写的好处是绕过 XML dialplan,路由逻辑集中在外部程序里。

另一种方式是把外呼号码塞进字符串里直接打在 dialplan 上:

originate sofia/gateway/gsm_gateway/13800000000 10086 XML public

前一个串是外呼的腿,后一个串10086作为对端看到的主叫,XML表示使用 XML dialplan,public是 context。这条命令会让 FreeSWITCH 先呼出到网关,再把收到的来电作为新呼叫路由进publiccontext。如果网关回 486,会直接导致原有呼叫失败,日志里能看到具体原因,这类外呼的排错重点在网关响应码,而不在 dialplan。

4.3 呼出号码清洗:前缀、加码和编码转换

外呼路由不能只做透明转发。网关设备通常对号码格式有要求,比如本地网内呼叫要加0或9,异地手机要加0,固话要加区号,国际号码要加00。这些清洗工作放在 dialplan 里做,比放外部程序里改更直观。

我的习惯是把号码清洗放独立 extension,用条件分支区分号码段:

<extension name="out_local_fixed"> <condition field="destination_number" expression="^9(\d{7,8})$"> <action application="set" data="out_number=0$1"/> <action application="bridge" data="sofia/gateway/gsm_gateway/${out_number}"/> </condition> </extension> <extension name="out_mobile"> <condition field="destination_number" expression="^91[3-9]\d{9}$"> <action application="set" data="out_number=$1"/> <action application="bridge" data="sofia/gateway/gsm_gateway/${out_number}"/> </condition> </extension>

正则里的捕获组在这里承担了"剥壳"任务:用户拨 901012345678,实际发到网关的是001012345678。清洗规则因网关而异,这个例子代表的是最常见的"去掉前缀、补区号"套路。重点是set后的变量在bridge里用${out_number}引用,中间隔了多条 action 也没问题,因为变量挂在 channel 上。

4.4 外呼失败的重试与备用路由

生产环境最不能忍的就是单条中继故障导致整个外呼瘫痪。FreeSWITCH 原生支持 gateway 级 failover:在 dialplan 里写多个 bridge 是无效的,正确的做法是用bridge的逗号语法,多个出口用逗号分隔,FreeSWITCH 会按顺序逐个尝试:

<action application="bridge" data="sofia/gateway/gsm_gateway/$1,sofia/gateway/backup_pstn/$1"/>

这套机制的原理是 bridge 执行时会逐条尝试呼叫,前一条失败立即释放并进入下一条。可用于跨区域网关容灾,也可用于主备中继切换。要注意的是如果两个网关同时振铃(而不是先后尝试),需要用多腿 bridge 的复杂语法,日常主备切换用逗号分隔就够了。

5. 呼入呼出路由配置避坑:正则、变量、认证与 NAT 的五个典型翻车现场

5.1 拨号计划正则匹配不生效

现象:明明写了<condition field="destination_number" expression="^1000$">,拨打 1000 却没有匹配到任何 extension,控制台显示 No dialplan match。

原因:呼叫进入的 context 不对。比如 external profile 配置的 context 是public,但你改的 dialplan 写在了defaultcontext 下,路由永远不可能命中。

解决:在vars.xml里确认default_areacode和domain值,然后在对应 profile 的context参数上排查。我常用的排查顺序是:先sofia status profile external查看当前 profile 绑定的 context,再对着 dialplan 里同名的<context>块检查。另外^1000$这类全匹配是对的,但要留意 FreeSWITCH 某些版本会把destination_number带上额外的前缀字符,用第 3 章的 log 打印确认实际值再做正则。

5.2 分机注册成功但外呼 403/404

现象:内部分机之间互拨正常,一旦拨打 9 开头的号码,网关返回 403 Forbidden 或 404 Not Found,而网关设备直连分机测试没有任何问题。

原因:对等中继模式下,网关虽然不要求注册,但对主叫号码做了白名单校验。FreeSWITCH 默认会把注册分机的号码作为主叫发过去,网关不认识就拒了。

解决:dialplan 里在外呼动作前强制改写effective_caller_id_number:

<action application="set" data="effective_caller_id_number=实际放行号码"/>

注意这里要写在 bridge 之前,否则改写只对后续应用生效,通道已经带着原主叫号发出去了。

5.3 外呼显示的主叫号码被网关吞掉

现象:dialplan 里设置了effective_caller_id_number=10086,bridge 到网关后,对方手机上看到的仍是乱码或空号。

原因:网关未必使用来自 FreeSWITCH 的 Caller-ID 头。部分 GSM 网关要求单独通过 SIP 扩展头传主叫,还有的网关直接用自己的通道号做为主叫。

解决:翻阅网关配置手册,找"主叫透传"或"通道主叫设置"项。FreeSWITCH 侧能做的只是把effective_caller_id_number写好,同时保证 bridge 前没有别的模块改写它。如果网关走 SMPP 或私有协议对接,这个工作就更多取决于网关设备,FreeSWITCH 只是把该传的字段传到。

5.4 内部呼入正常,外部呼入却只能听到回铃没有声音

现象:从 SIP 话机注册到 internal profile 后互拨正常,但外部 PSTN 呼入到外呼落地分机后能接通,双方向没有任何声音。

原因:外部 profile 和内部 profile 用了不同的 RTP 端口区间,或者 NAT 穿透没有配置。FreeSWITCH 默认 RTP 端口区间写在switch.conf.xml里,如果 external profile 的rtp-ip没绑对,媒体流会发到错误地址。

解决:确认 external profile 的rtp-ip和sip-ip指向同一个本机 IP,并在switch.conf.xml里检查rtp-start-port与rtp-end-port,确保防火墙放行了整个区间。NAT 环境下需要开ext-rtp-ip和ext-sip-ip,否则媒体流地址是内网 IP,对端回包找不到路。

5.5 拨打总机号码却进了语音信箱

现象:呼入路由明明写的是 IVR 播放,实际却直接进了某个分机语音信箱,日志里能看到拨号计划匹配到了 voicemail 相关 extension。

原因:dialplan 的 extension 匹配是按顺序进行的,如果前面有一个匹配范围更大的 extension 先命中了,后面的 IVR 规则根本没机会执行。

解决:把精确匹配的 extension 放在前面,通配或兜底规则放最后。比如^6000$要比^6\d{3}$更靠前,否则 6000 会被后面的范围匹配先吃掉。排查时用第 3 章的 log 打印当前命中的 extension name,能立刻看出来是哪条规则截胡。

6. 用控制台和日志验证一套呼入呼出:从拨号到挂断的完整检查习惯

配置完成后不要直接连接生产网关,先在测试环境走一遍完整的呼入呼出验证。我的固定流程分三步:确认 profile 状态、观察呼叫日志、核对 CDR 输出。

第一步,确认 sofia profile 和网关状态:

sofia status sofia profile external status sofia gateway status gsm_gateway

正常状态下网关状态应该显示State: REGED或State: NOREG,前者是注册模式,后者是对等中继模式(register=false)的预期结果。如果注册模式显示 REGED,说明账号认证通过了;如果长时间停留在State: Error,问题基本出在账号密码或 proxy 地址。

第二步,一边发起呼叫一边在 fs_cli 里开日志观察:

console log info

呼入时重点看三行:INVITE 到达的行会打印 Request-URI;进入 dialplan 后能看到匹配到的 extension name;bridge 执行后能看到通道 UUID 和目的网关地址。外呼时重点看网关侧返回的 SIP 响应码:100 Trying后的180 Ringing表示网关已振铃,483 Too Many Hops或503 Service Unavailable多半是网关链路问题,而486 Busy Here是远端拒绝。这些响应码信息量远比 "no route" 这类模糊日志大。

第三步,挂断后查 CDR,确认路由结果。V1.2.7 的 CDR 模块默认打开,存放路径在log/下:

fs_cli -x "show channels as json" | head -20

CDR 里能看到的字段比较有限,重点关注destination_number、context、hangup_cause三项。hangup_cause是呼出排查的第一线索:NORMAL_CLEARING表示正常挂断,NO_ANSWER说明对端没接,ORIGINATOR_CANCEL说明主叫先挂了,INVALID_NUMBER_FORMAT则是号码清洗或正则的问题。

整套配置从接线到验收,我吃过最大的亏是在没有抓sofia profile external的 context 是否匹配的情况下猛改 dialplan。从那以后我每次新增呼入或外呼路由,都会强制自己先在 profile 上确认 context 名,再用 console log 走一遍真实呼叫,最后核对 CDR 的 hangup_cause 才敢交给业务侧接入。这套流程虽然慢,但能挡住九成以上的低级错误。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询