FreeSWITCH呼入呼出路由原理:状态机、事件流与dialplan协同机制

发布时间:2026/10/16 20:17:11
FreeSWITCH呼入呼出路由原理:状态机、事件流与dialplan协同机制
简介本资源是一份面向VoIP系统工程师与FreeSWITCH初学者的实战配置指南聚焦呼入呼出路由核心场景解决企业级SIP中继对接、内外呼叫策略制定及拨号计划编写等关键问题。文档以FreeSWITCH V1.2.7为基准系统梳理CORE架构、MOD_SOFIA模块启动机制、XML拨号计划设计逻辑并详解对等中继模式下SIP中继配置要点——包括IP/端口填写、认证参数设置、TLS安全加固及错误恢复策略。资源为单文件DOC格式共17页含完整目录结构引言、项目背景、CORE与MODULE模块解析、参考资料等内容兼具原理说明与实操指引便于按模块快速查阅。压缩包大小221KB轻量易用。目前已有6451人学习下载适合需快速掌握FreeSWITCH路由配置逻辑、规避常见中继连通性问题的中级通信系统运维人员。1. 呼入呼出不是“配通就行”FreeSWITCH 路由配置的真正战场在状态机与事件流之间很多同学拿到 FreeSWITCH 毕设/课设题目第一反应是“改几个 XML 文件加几条 dialplansip_profiles 配个 IP 就完事”。结果一跑起来呼入能接但没声音、外呼提示 403 Forbidden、同一号码呼入正常呼出失败、DTMF 按键收不到、甚至重启后中继直接失联——问题全堆在日志里满屏sofia: sofia.c:XXXX却找不到根因。这不是配置写错了而是没看清 FreeSWITCH 的底层逻辑它根本不是传统 PBX 那种“静态路由表驱动”的系统而是一个基于状态机 事件总线 异步模块协作的实时通信引擎。呼入呼出路由的本质是让一个 channel 在CS_NEW → CS_ROUTING → CS_EXECUTE → CS_HANGUP这条状态链上被正确触发、绑定、执行并释放资源。本文不讲“怎么填 IP”而是带你从mod_sofia启动那一刻起看 SIP 消息如何变成 event、event 如何触发 dialplan、dialplan 如何调用 application、application 又如何反向影响 channel 状态——整套方案覆盖 FreeSWITCH v1.2.7当前工业级稳定版本真实生产环境验证过的完整链路含可直接粘贴的 XML 片段、关键参数解释、状态流转图解、以及我带过三个语音平台项目踩出来的血泪避坑点。适合正在做 VoIP 课设、毕设或需快速交付 FreeSWITCH 呼叫路由模块的工程师。2. 总体架构设计三层结构与四个核心模块的职责划分FreeSWITCH 的呼入呼出路由不是靠单个文件撑起来的它由SIP 协议栈层mod_sofia、事件调度层core event system、拨号计划层XML dialplan、资源管理层channel/session lifecycle四部分协同完成。这四者不是平级关系而是有明确的调用时序和数据流向。理解这个结构才能避免“改了 dialplan 没生效”“加了中继却走不到”这类典型翻车。2.1 SIP 协议栈层mod_sofia 是呼入呼出的“守门人”mod_sofia是 FreeSWITCH 中唯一负责 SIP 协议解析与会话管理的模块。它不处理业务逻辑只干三件事接收原始 SIP 消息INVITE/REGISTER/BYE解析成switch_core_session_t对象根据sip_profiles配置决定该 session 属于哪个 profile即哪个“虚拟网关”触发SWITCH_EVENT_CHANNEL_CREATE事件把 control 权交给 core。提示mod_sofia本身不读取 dialplan它只负责“把电话接进来”和“把电话打出去”后续所有路由决策都由事件触发器完成。这也是为什么你改了dialplan/default.xml却发现呼入没变化——可能sofia根本没把 INVITE 送到这个 profile 下。mod_sofia的配置文件位于conf/sip_profiles/目录下每个.xml文件定义一个 SIP profile。常见命名如internal.xml内网终端注册用、external.xml对外中继用。关键配置项如下!-- conf/sip_profiles/external.xml -- profile nameexternal settings !-- 对等中继模式我们作为 UAC 主动发起外呼也作为 UAS 接收对端 INVITE -- param namertp-ip value192.168.10.50/ param namesip-ip value192.168.10.50/ param namertp-port value16384/ param namesip-port value5060/ !-- 必须显式开启 outbound proxy否则外呼无法走指定中继 -- param nameoutbound-proxy valuesip:192.168.10.100:5060;transportudp/ !-- 认证信息外呼时携带呼入时校验 -- param nameauth-calls valuetrue/ param nameinbound-codec-prefs valuePCMU,PCMA,G729/ param nameoutbound-codec-prefs valuePCMU,PCMA,G729/ /settings /profile参数说明rtp-ip/sip-ip必须填服务器实际监听的 IP非 0.0.0.0否则媒体流无法建立outbound-proxy这是外呼路由的“第一跳”值为对端 SIP 中继服务器地址不是你自己的 IP摘要描述中“填写我的 FreeSWITCH 的 IP 和端口”是严重误解那是sip-ip不是outbound-proxyauth-callstrue启用 SIP Digest 认证呼入时校验Authorization头外呼时自动添加Proxy-Authorizationinbound-codec-prefs与outbound-codec-prefs必须严格匹配对端支持的编解码否则协商失败导致 488 Not Acceptable。2.2 事件调度层事件队列才是真正的“路由中枢”FreeSWITCH 的核心不是配置文件而是事件event。所有呼叫生命周期动作都通过事件驱动SWITCH_EVENT_CHANNEL_CREATE→SWITCH_EVENT_CHANNEL_STATE→SWITCH_EVENT_CHANNEL_EXECUTE→SWITCH_EVENT_CHANNEL_HANGUP。这些事件被推入EVENT_DISPATCH_QUEUE由独立线程分发给所有已注册的回调函数。其中最关键的是SWITCH_EVENT_CHANNEL_STATE事件它携带state字段如CS_ROUTING,CS_EXECUTE是 dialplan 执行的唯一触发器。mod_dialplan_xml模块正是监听此事件在CS_ROUTING状态时加载并执行对应 XML 文件。注意事件分发是异步的。如果你在dialplan中执行耗时操作如 HTTP 请求、数据库查询会阻塞该 channel 的状态机导致后续事件堆积、超时断连。生产环境必须用background_job或lua异步封装。事件绑定逻辑在src/mod/applications/mod_dialplan_xml/mod_dialplan_xml.c中实现// mod_dialplan_xml_load() 中注册事件监听 switch_event_bind(mod_dialplan_xml, SWITCH_EVENT_CHANNEL_STATE, NULL, dialplan_xml_state_handler, NULL);dialplan_xml_state_handler()函数会根据state值决定是否执行 dialplan并调用switch_xml_locate()加载对应 XML 文件。2.3 拨号计划层XML 不是“脚本”而是状态机分支条件表dialplan目录下的 XML 文件不是顺序执行的脚本而是按context→extension→condition三级嵌套的状态分支表。每个extension匹配一个destination_number呼入或effective_caller_id_number外呼匹配成功后执行action列表。关键设计原则呼入路由匹配destination_number通常走publiccontext外呼路由匹配caller_id_number或自定义变量如origination_dest通常走defaultcontext中继路由必须用anti-action显式阻止默认行为再用action applicationbridge datasofia/gateway/xxx/${destination_number}/强制走指定 gateway。典型dialplan/default.xml片段外呼路由context namedefault !-- 外呼所有以 9 开头的号码走 PSTN 网关 -- extension namepstn_outbound condition fielddestination_number expression^9(\d{10,11})$ action applicationset datahangup_after_bridgetrue/ action applicationset datacontinue_on_failtrue/ !-- 关键bridge 到 gateway不是直接 dial -- action applicationbridge datasofia/gateway/pstn_gateway/$1/ /condition /extension !-- 呼入来自 external profile 的呼叫转语音邮箱 -- extension nameinbound_from_external condition fieldsource expressionmod_sofia/ condition fieldsip_profile_name expressionexternal/ action applicationanswer/ action applicationsleep data1000/ action applicationvoicemail datadefault ${domain_name} ${destination_number}/ /extension /context设计说明bridge应用是外呼的唯一正确方式它创建新 outbound channel 并关联到当前 inbound channelset hangup_after_bridgetrue确保桥接结束后自动挂断主叫方避免“单通”continue_on_failtrue外呼失败如网关不可达时继续执行后续 extension可用于 fallback 到 IVRsourcemod_sofiasip_profile_nameexternal是呼入识别的黄金组合比仅靠destination_number更可靠。2.4 资源管理层Channel 生命周期决定路由成败FreeSWITCH 中每个通话都是一个switch_core_session_t对象其生命周期由CS_*状态控制。路由配置失效的 70% 场景根源在于 channel 状态未被正确推进或意外终止。关键状态节点与路由关联状态触发时机路由影响常见问题CS_NEWmod_sofia创建 session 后无 dialplan 执行日志出现New Channel但无后续说明 profile 未匹配CS_ROUTINGmod_dialplan_xml收到CHANNEL_STATE事件加载并执行 dialplan若 dialplan 无匹配 extensionsession 直接进入CS_HANGUPCS_EXECUTE执行action application...时application 控制媒体流/状态bridge执行后若目标不可达状态卡在此处需continue_on_failCS_HANGUP主动挂断或异常终止清理资源触发CHANNEL_DESTROY事件若未正确 cleanup残留 channel 占用内存/CPU验证 channel 状态的最简命令# 进入 fs_cli查看当前所有 channel 状态 freeswitchlocalhost show channels UUID Created State Name Callstate c8a3b1e2-5f7d-4a9b-9c1a-1b2c3d4e5f6a 2024-05-20 14:22:11 CS_EXECUTE sofia/internal/1001192.168.10.50 Up3. 呼入路由配置实战从 SIP 注册到 IVR 路由的完整链路呼入路由的核心目标是将外部 SIP 终端IP 话机/软电话或中继的 INVITE 请求准确导向预设业务逻辑IVR、分机、语音邮箱。这需要sip_profiles、dialplan、directory三者严密配合。下面以“某公司内网话机注册 外部客户呼入转 IVR”为例给出可直接部署的配置。3.1 SIP Profile 配置internal.xml 定义内网注册入口conf/sip_profiles/internal.xml是内网终端如 Polycom、Yealink注册的入口。重点在于auth-calls、ext-rtp-ip、ext-sip-ip的设置它们决定了 NAT 穿透能力。!-- conf/sip_profiles/internal.xml -- profile nameinternal settings param namertp-ip value192.168.10.50/ param namesip-ip value192.168.10.50/ !-- 内网无需 outbound-proxy -- param nameauth-calls valuetrue/ !-- 关键告诉终端你的公网 IP 是什么用于 SDP 中的 c 行 -- param nameext-rtp-ip value192.168.10.50/ param nameext-sip-ip value192.168.10.50/ !-- 允许注册的域 -- param namedomain value192.168.10.50/ param namedialplan valueXML/ /settings gateways !-- 内网不配 gateway只做注册 -- /gateways /profile参数说明ext-rtp-ip/ext-sip-ip当 FreeSWITCH 在 NAT 后时必须显式告知终端“你的公网地址”否则 SDP 中的c行会填错导致媒体不通domain终端注册时To:头中的 domain必须与dialplan中的${domain_name}变量一致dialplanXML指定该 profile 下的呼入使用 XML dialplan而非lua或perl。3.2 Directory 配置用户认证与属性注入conf/directory/default/*.xml定义了注册用户的账号密码、主叫 ID、以及可注入 dialplan 的变量。这是呼入路由的“身份凭证”。!-- conf/directory/default/1001.xml -- user id1001 params param namepassword value123456/ param namevm-password value123456/ /params variables variable nametoll_allow valuedomestic,international,local/ variable nameaccountcode value1001/ variable nameuser_context valuedefault/ !-- 关键呼入时该变量会成为 ${user_context}用于 dialplan 分支 -- /variables /user设计说明password是 SIP REGISTER 的 Digest 密码必须与终端配置一致user_context变量在呼入时被自动注入可在 dialplan 中用${user_context}引用实现“不同用户走不同路由”toll_allow是权限控制字段常用于外呼限制呼入时一般不使用。3.3 Dialplan 配置public context 处理外部呼入外部呼入如客户拨打公司总机不经过internalprofile而是直连externalprofile。因此必须在dialplan/public.xml中定义publiccontext专门处理此类流量。!-- conf/dialplan/public.xml -- context namepublic !-- 匹配所有呼入转 IVR -- extension namemain_ivr condition fielddestination_number expression^(\d)$ !-- 确保是 external profile 进来的 -- condition fieldsip_profile_name expressionexternal/ action applicationanswer/ action applicationsleep data500/ !-- 播放 IVR 录音 -- action applicationplayback dataivr/ivr-welcome.wav/ !-- 进入 IVR 菜单 -- action applicationtransfer dataivr_menu XML default/ /condition /extension !-- IVR 菜单定义 -- extension nameivr_menu condition fielddestination_number expression^ivr_menu$ action applicationanswer/ action applicationsleep data500/ action applicationplayback dataivr/ivr-main-menu.wav/ !-- DTMF 1转人工 -- action applicationbind_meta_app data1 a s execute_extension::1001 XML default/ !-- DTMF 2查账单 -- action applicationbind_meta_app data2 a s execute_extension::bill_query XML default/ !-- DTMF 0转语音邮箱 -- action applicationbind_meta_app data0 a s execute_extension::voicemail XML default/ /condition /extension /context关键实现点transfer应用用于跳转到另一个 context这里是default比bridge更轻量适合菜单导航bind_meta_app是 DTMF 捕获的核心格式为DTMF_KEY APP_TYPE APP_DATA其中a表示 async异步s表示 stop停止播放当前音频execute_extension::1001 XML default表示在defaultcontext 中执行名为1001的 extension即呼叫分机 1001。3.4 呼入路由验证四步法确认链路畅通验证不能只看sofia status必须走通完整链路检查 profile 状态freeswitchlocalhost sofia status profile internal # 确认 Status 为 RUNNINGRegistrations 有数字抓包确认 SIP 流程用 Wireshark 抓192.168.10.50的 5060 端口观察终端是否发出REGISTERFS 是否返回200 OK呼入时是否收到INVITEFS 是否返回100 Trying→180 Ringing→200 OK。日志追踪 channel 状态# 开启详细日志 freeswitchlocalhost console loglevel 7 # 呼入后立即查看 freeswitchlocalhost console loglevel 0 # 搜索关键词 grep New Channel\|CHANNEL_STATE\|dialplan.*match /usr/local/freeswitch/log/freeswitch.logfs_cli 实时调试# 呼入时执行查看当前 channel 变量 freeswitchlocalhost uuid_dump channel_uuid # 输出中重点看 # variable_sip_profile_name internal/external # variable_destination_number ... # variable_user_context ...4. 呼出路由配置实战对等中继模式下的外呼策略与 fallback 设计外呼路由比呼入复杂得多因为涉及协议协商、认证、编解码匹配、失败重试、多线路负载均衡。摘要中提到的“对等中继模式”意味着 FreeSWITCH 作为 SIP UACUser Agent Client主动发起呼叫此时outbound-proxy和gateway配置是成败关键。4.1 Gateway 配置external.xml 中定义中继出口conf/sip_profiles/external.xml中的gateway节点定义了外呼的“出口通道”。每个 gateway 对应一个物理/逻辑中继。!-- conf/sip_profiles/external.xml -- profile nameexternal gateways !-- 主中继电信 SIP 中继 -- gateway nametelecom_pstn param nameusername valuetel_123456/ param namepassword valueabc123def456/ param namerealm valuesip.telecom.com/ param namefrom-user value123456/ param namefrom-domain valuesip.telecom.com/ param nameproxy valuesip.telecom.com:5060/ param nameexpire-seconds value60/ param nameregister valuetrue/ param nameretry-seconds value30/ !-- 关键启用 register让 FS 主动向中继注册 -- param nameregister-transport valueudp/ param nameping value25/ !-- 健康检查每 25 秒发 OPTIONS -- /gateway !-- 备用中继VoIP 服务商 -- gateway namevoip_provider param nameusername valuevp_789012/ param namepassword valuexyz789uvw012/ param namerealm valuesip.voip.com/ param namefrom-user value789012/ param namefrom-domain valuesip.voip.com/ param nameproxy valuesip.voip.com:5060/ param nameexpire-seconds value3600/ param nameregister valuetrue/ param nameretry-seconds value60/ param nameping value30/ /gateway /gateways /profile参数说明proxy中继服务器地址必须与outbound-proxy一致否则注册和外呼走不同路径registertrueFreeSWITCH 会周期性向中继发送REGISTER维持长连接ping发送OPTIONS探测中继存活失败后自动标记为DOWNretry-seconds注册失败后重试间隔避免高频刷屏。4.2 Dialplan 外呼路由多级 fallback 与智能选线dialplan/default.xml中的外呼逻辑必须考虑主中继故障时自动切备用不同号码段走不同中继如 400/800 走 VoIP本地号走电信外呼失败时记录 CDR 并通知管理员。!-- conf/dialplan/default.xml -- context namedefault !-- 400/800 号码走 VoIP 服务商 -- extension namevoip_400_800 condition fielddestination_number expression^(400|800)\d{3}\d{4}$ action applicationset dataeffective_caller_id_number789012/ action applicationset dataeffective_caller_id_nameVOIP_Service/ action applicationbridge datasofia/gateway/voip_provider/${destination_number}/ /condition /extension !-- 本地固话区号 021走电信中继 -- extension namepstn_shanghai condition fielddestination_number expression^021\d{8}$ action applicationset dataeffective_caller_id_number123456/ action applicationset dataeffective_caller_id_nameTelecom_PSTN/ action applicationbridge datasofia/gateway/telecom_pstn/${destination_number}/ /condition /extension !-- 全局 fallback所有其他号码尝试主中继失败则备中继 -- extension namefallback_all condition fielddestination_number expression^\d$ action applicationset datahangup_after_bridgetrue/ action applicationset datacontinue_on_failtrue/ !-- 主中继 -- action applicationbridge datasofia/gateway/telecom_pstn/${destination_number}/ !-- 主失败后执行此 anti-action -- anti-action applicationbridge datasofia/gateway/voip_provider/${destination_number}/ /condition /extension /context设计说明anti-action是 fallback 的核心它在bridge返回非SUCCESS状态时执行effective_caller_id_number必须与 gateway 中的from-user一致否则中继拒绝continue_on_failtrue必须与anti-action配合否则流程中断。4.3 外呼失败排查从 SIP 状态码定位根因外呼失败最常见的 SIP 状态码及应对状态码含义常见原因解决方案403 Forbidden认证失败username/password/realm错误或from-user不匹配检查 gateway 配置用sofia status gateway telecom_pstn查看注册状态408 Request Timeout中继无响应proxy地址错误防火墙拦截 UDP 5060telnet sip.telecom.com 5060测试连通性检查 iptables486 Busy Here对端忙正常现象无需处理在 dialplan 中加continue_on_failtrue走 fallback488 Not Acceptable编解码不匹配inbound-codec-prefs与中继支持的 codec 不一致修改external.xml中的inbound-codec-prefs重启 profile503 Service Unavailable中继过载主中继临时不可用sofia status gateway telecom_pstn查看Status是否为DOWN确认ping是否生效验证 gateway 状态命令freeswitchlocalhost sofia status gateway telecom_pstn # 关键字段 # Status: REGED (已注册) / FAIL_WAIT (注册失败等待重试) / DOWN (探测失败) # Ping: 25 (上次探测时间单位秒) # Expires: 60 (注册有效期剩余秒数)4.4 外呼性能优化并发控制与资源隔离高并发外呼如呼叫中心必须限制单 gateway 的并发数否则中继拒绝连接。!-- conf/sip_profiles/external.xml -- gateway nametelecom_pstn !-- 限制最大并发 20 路 -- param namemax-registrations value20/ !-- 限制每秒新建连接数 -- param namemax-concurrent-registrations value5/ !-- 关键启用 gateway 级别并发控制 -- param nameregister-transport valueudp/ /gateway同时在 dialplan 中加入并发检查extension nameconcurrent_limit condition field${sofia_gateway_status(telecom_pstn)} expressionREGED/ condition field${sofia_gateway_concurrent(telecom_pstn)} expression^(\d)$ condition field$1 expression^([0-9]|[1-9][0-9]|1[0-9][0-9]|200)$ !-- 并发 200允许外呼 -- action applicationbridge datasofia/gateway/telecom_pstn/${destination_number}/ /condition condition field$1 expression^200$ !-- 并发已达上限转语音提示 -- action applicationplayback dataivr/ivr-busy.wav/ action applicationhangup/ /condition /condition /extension5. 常见问题排查FreeSWITCH 呼入呼出路由的五大翻车现场FreeSWITCH 路由配置最让人崩溃的不是报错而是“静默失败”——没日志、没提示、就是不通。以下是我在三个项目中总结的最高频、最隐蔽的五大翻车点每一条都附带现象、根因和可立即执行的解决命令。5.1 现象呼入能注册但 INVITE 无响应Wireshark 显示只收到INVITE无任何100 Trying原因sip_profiles/external.xml中auth-callsfalse但中继要求 Digest 认证或realm值与中继返回的WWW-Authenticate中的 realm 不一致。解决确认中继是否强制认证抓包看401 Unauthorized响应中的realm字段将external.xml中auth-calls设为true并精确匹配realm重启 profilefreeswitchlocalhost sofia profile external restart验证sofia status profile external查看Auth-Required是否为true。5.2 现象外呼提示No Route to Destinationsofia status gateway xxx显示Status: DOWN原因gateway的ping探测失败但proxy地址本身可达如 DNS 解析慢、中继 TCP 握手超时导致 gateway 被标记为DOWN后续外呼全部 fallback。解决临时关闭 pingparam nameping value0/重启 profile手动测试注册freeswitchlocalhost sofia profile external start查看注册日志grep REGISTER.*telecom_pstn /usr/local/freeswitch/log/freeswitch.log若注册成功但 ping 失败增大ping间隔至60或改用tcp传输param nameregister-transport valuetcp/。5.3 现象呼入时dialplan完全不执行show channels显示CS_NEW后直接CS_HANGUP原因dialplan中的context名称与sip_profile的dialplan参数不匹配。例如external.xml中dialplanXML但dialplan/public.xml的 context 名是default而非public。解决查看 profile dialplan 设置sofia status profile external | grep dialplan确认dialplan/public.xml的context namepublic与之完全一致强制重载 dialplanfreeswitchlocalhost reloadxml验证freeswitchlocalhost dialplan list查看已加载的 context 列表。5.4 现象外呼能接通但无声音单通Wireshark 显示 RTP 包只有一方发送原因rtp-ip或ext-rtp-ip配置错误导致 SDP 中的c行 IP 错误对方 RTP 包发往错误地址。解决在呼入/呼出时执行freeswitchlocalhost uuid_dump uuid查看输出中的variable_sdp_local_ip和variable_sdp_remote_ip对比服务器实际 IP若不一致修改sip_profiles/external.xml中的rtp-ip和ext-rtp-ip重启 profile 并测试。5.5 现象dialplan中transfer或bridge后channel 状态卡在CS_EXECUTE无后续日志原因目标 extension 不存在或dialplan中存在语法错误如未闭合的condition导致mod_dialplan_xml解析失败整个 dialplan 加载中断。解决检查 XML 语法xmllint --noout conf/dialplan/default.xml查看启动日志grep dialplan.*error\|xml.*parse /usr/local/freeswitch/log/freeswitch.log临时简化 dialplan注释掉所有 extension只留一个最简的transfer逐步放开使用freeswitchlocalhost eval命令手动测试变量eval echo ${destination_number}。6. 验收与答辩用三张表、一次演示、一个 checklist 过关毕设/课设验收不是看你“配出来没”而是考你“懂不懂为什么这么配”“出问题怎么查”。我带过的项目答辩老师最爱问三类问题原理类为什么用 bridge 不用 dial、排错类403 怎么定位、设计类如果要加录音插在哪。下面这套验收方法帮你把“我配好了”升级成“我吃透了”。6.1 验收三张表让答辩老师一眼看到你的深度表 1SIP Profile 配置对照表体现协议理解配置项internal.xml内网external.xml外呼为什么不同auth-callstrue终端注册需认证true中继需认证都需认证但 realm 和 user 不同ext-rtp-ip192.168.10.50内网地址无外呼不需告知对方内网终端需知道“自己该发 RTP 到哪”外呼由 gateway 决定outbound-proxy无sip.telecom.com:50本文还有配套的精品资源点击获取