
简介本资源是一份面向VoIP开发工程师与通信系统运维人员的FreeSWITCH实战配置指南聚焦呼入呼出路由核心场景解决企业级SIP中继对接、内外呼叫策略制定及拨号计划编写等关键问题。文档以FreeSWITCH V1.2.7为基准系统梳理CORE架构、MOD_SOFIA模块启动机制、XML拨号计划设计逻辑并详解对等中继模式下的SIP中继配置要点——包括IP/端口填写、认证参数设置、TLS安全加固及错误恢复策略。资源为单文件DOC文档221KB内容结构完整含引言、项目背景、组件架构、CORE与MODULE技术解析及参考资料便于按模块精读与快速查阅。目前已有6451人学习下载适合具备基础SIP协议认知的中级以上技术人员用于生产环境配置参考、故障排查辅助与拨号逻辑复盘。1. 呼入呼出不是“配通就行”FreeSWITCH 路由配置的真正卡点在状态机与拨号计划的耦合深度很多同学交 FreeSWITCH 毕设/课设时第一版配置跑通外呼、能接进一个 SIP 终端就以为“路由配置完成了”。结果答辩被问“为什么同一号码呼入走 IVR呼出却直连网关”、“为什么加了 T.38 传真路由后语音呼叫开始丢包”——当场哑火。根本原因不是语法写错而是把dialplan当成静态跳转表忽略了 FreeSWITCH 的核心设计哲学所有路由决策都发生在 channel 生命周期的状态机中而拨号计划只是状态钩子state hook的触发器。本文聚焦 V1.2.7 这个仍在大量生产环境使用的稳定版本不讲抽象原理只拆解真实项目里必须面对的四个硬骨头① 呼入时如何基于 caller_id、SIP 头字段、源 IP 网段做三级鉴权路由② 外呼时如何让同一 outbound profile 同时支持对等中继peer、注册中继register和动态网关选择③ 如何用extension的continuetrue和breakon-true实现条件嵌套而不陷入 XML 嵌套地狱④ 最关键的——当sofia模块收到 INVITE 后CHANNEL_STATE事件如何被switch_core_state_machine.c拦截并最终调用dialplan中定义的set、bridge、transfer应用。这不是教你怎么写 XML而是告诉你每行 dialplan 配置背后都有一个正在运行的线程、一个被锁住的 channel 句柄、一次对 runtime 内存池的 malloc以及一次对 hash 表的 key 查找。适合正在调试 SIP 中继不通、呼入被拒、外呼无音频、或需要交付可复现 VoIP 方案的从业者与学生。2. 总体架构设计三层状态驱动模型与四个核心配置域的职责划分FreeSWITCH 的路由能力不是靠堆砌 XML 实现的而是由底层状态机、中间模块层、上层配置层共同构成的三层驱动模型。V1.2.7 版本中这个模型已高度固化任何脱离该模型的“技巧性配置”都会在高并发或异常断连时暴露问题。我们必须先厘清这三层如何咬合再谈具体配置。2.1 三层状态驱动模型从 sofia 接收 INVITE 到执行 bridge 的完整链路当一个 SIP INVITE 报文到达mod_sofia它不会直接去查 dialplan。整个流程严格遵循以下状态跃迁Network Layer → Channel Creationsofia解析 INVITE 后调用switch_core_session_create()创建 channel 对象并初始化session-channel-state CS_NEWState Machine Hook Triggerswitch_core_state_machine.c检测到CS_NEW状态立即触发STATE_HANDLER(CS_NEW)该 handler 内部会调用switch_core_session_execute_application()执行dialplan中为new状态绑定的应用通常是set或exportDialplan Execution Context此时 dialplan 的context namepublic被加载extension nameunauthenticated匹配成功执行action applicationset datadomain_name$${domain}/—— 注意这个set不是简单赋值而是将键值对写入session-variableshash 表供后续所有状态钩子读取State Transition Next HookCS_NEW处理完毕channel 状态变为CS_INIT再次触发STATE_HANDLER(CS_INIT)此时 dialplan 中contextpublic下匹配destination_number的extension开始执行可能调用bridge、transfer或ivrMedia Path Decision若执行bridgeswitch_ivr_bridge.c会根据session-variables[effective_caller_id_number]和session-variables[sip_to_user]查询directory模块决定目标 endpoint 类型SIP、PSTN、conference并启动 RTP 协商。提示这就是为什么你在dialplan里写的set必须放在new状态的 extension 中——因为只有CS_NEW钩子能确保变量在 channel 生命周期早期写入后续状态如CS_ROUTING再读取才有效。把set放在routing状态的 extension 里等于变量写入时 channel 已经开始寻路大概率失效。2.2 四个核心配置域分工明确越界即翻车FreeSWITCH 的配置不是散装文件而是有强职责边界的四个域每个域修改都需理解其影响范围配置域物理路径核心职责修改风险等级典型误操作SIP Profileconf/sip_profiles/external.xml定义 SIP 信令层参数监听地址/端口、TLS 配置、认证方式、NAT 策略、keepalive 间隔⚠️⚠️⚠️ 高在 external.xml 中开启inbound-codec-negotiationtrue导致呼入编解码协商失败Dialplanconf/dialplan/public.xml定义 channel 状态跃迁时的业务逻辑号码匹配、变量设置、媒体控制、路由分发⚠️⚠️ 中用condition fielddestination_number expression^1\d{10}$匹配手机号但未加breakon-false导致后续 extension 也被错误执行Directoryconf/directory/default.xml定义用户/终端元数据SIP URI、密码、语音邮箱、T.38 支持标识、ACL 权限⚠️ 低修改default.xml后忘记执行reloadxml导致新用户无法注册Gatewayconf/sip_profiles/external/gateway_name.xml定义外呼中继参数远端 SIP 服务器地址、注册账号、重试策略、codec 列表、ping 间隔⚠️⚠️⚠️ 高在 gateway 配置中设置registerfalse却未在dialplan中显式调用sofia_contact导致外呼无 Contact 头注意V1.2.7 的modules.conf文件位于conf/autoload_configs/modules.conf决定了哪些模块被加载。如果你要启用mod_xml_curl动态拉取 dialplan必须在此文件中取消注释load modulemod_xml_curl/否则所有X-PRE-PROCESS cmdcurl ...指令将被忽略。2.3 呼入路由的核心设计基于三层鉴权的 context 分流机制真实项目中呼入流量绝非单一来源。你需要区分公网 SIP 中继来的电话、内网软电话注册的电话、WebRTC 通过 WebSocket 连入的电话。FreeSWITCH 用contextcaller_idnetwork_addr三者组合实现精准分流!-- conf/dialplan/public.xml -- context namepublic !-- 步骤1拦截所有呼入统一打标签 -- extension nametag_inbound_source condition fieldnetwork_addr expression^192\.168\.10\..*$ action applicationset datainbound_sourceinternal/ action applicationset dataacl_resultALLOW/ /condition condition fieldnetwork_addr expression^203\.0\.113\..*$ action applicationset datainbound_sourcedmz_sip_trunk/ action applicationset dataacl_resultCHECK_ACL/ /condition condition fieldsip_user_agent expression^WebRTC.*$ action applicationset datainbound_sourcewebrtc/ action applicationset dataacl_resultALLOW/ /condition /extension !-- 步骤2基于标签分流到不同 context -- extension nameroute_by_source condition field${inbound_source} expressioninternal action applicationtransfer data1000 XML internal/ /condition condition field${inbound_source} expressiondmz_sip_trunk action applicationtransfer data9000 XML trunk/ /condition condition field${inbound_source} expressionwebrtc action applicationtransfer data8000 XML webrtc/ /condition /extension /context这段配置的关键在于它不依赖destination_number做初始路由而是先识别来源再 transfer 到对应 context。这样做的好处是internalcontext 可以专注处理分机互拨无需鉴权trunkcontext 可以强制校验sip_from_user是否在白名单内webrtccontext 可以加载mod_verto特有应用。避免了在一个 context 里堆砌几十个condition逻辑清晰且易于审计。2.4 外呼路由的核心设计单 profile 多 gateway 的动态选择策略外呼最常踩的坑是为每个运营商建一个独立 profile如chinaunicom.xml,chinamobile.xml结果 dialplan 里全是bridge sofia/external/chinaunicom/$1导致维护成本爆炸。V1.2.7 的正确做法是一个externalprofile多个 gateway用 dialplan 动态选择。首先在conf/sip_profiles/external/下创建三个 gateway 文件!-- conf/sip_profiles/external/gw_unicom.xml -- gateway namegw_unicom param nameusername valueuni123/ param namepassword valuepass456/ param namerealm valuesip.unicom.example/ param namefrom-user valueuni123/ param nameproxy valuesip.unicom.example:5060/ param nameexpire-seconds value600/ param nameregister valuetrue/ param nameretry-seconds value30/ param namecontext valuepublic/ /gateway!-- conf/sip_profiles/external/gw_mobile.xml -- gateway namegw_mobile param nameusername valuemob789/ param namepassword valuepass012/ param namerealm valuesip.mobile.example/ param namefrom-user valuemob789/ param nameproxy valuesip.mobile.example:5060/ param nameexpire-seconds value600/ param nameregister valuetrue/ param nameretry-seconds value30/ param namecontext valuepublic/ /gateway然后在 dialplan 中根据被叫号码前缀自动选择 gateway!-- conf/dialplan/public.xml -- context namepublic extension nameoutbound_route_selector condition fielddestination_number expression^13[0-9]\d{8}$|^15[0-9]\d{8}$|^18[0-9]\d{8}$ action applicationset dataoutbound_gatewaygw_mobile/ /condition condition fielddestination_number expression^17[0-9]\d{8}$|^14[0-9]\d{8}$ action applicationset dataoutbound_gatewaygw_unicom/ /condition condition fielddestination_number expression^0[1-9]\d{1,2}-\d{7,8}$ action applicationset dataoutbound_gatewaygw_unicom/ /condition /extension extension nameexecute_outbound condition fielddestination_number expression^(\?1[2-9]\d{9}|1[2-9]\d{9}|0[1-9]\d{1,2}-\d{7,8}|1[3-9]\d{9})$ action applicationbridge datasofia/external/${outbound_gateway}/$1/ /condition /extension /context这里的关键设计点是outbound_gateway变量在execute_outbound扩展中被引用而bridge应用会自动解析${outbound_gateway}为实际 gateway 名称。FreeSWITCH 在执行bridge时会先查找sofia模块中已注册的 gateway 列表再根据名称加载其参数。这意味着你无需重启 FS 就能增删 gateway 文件只要reloadxml即可生效。3. 关键模块实现sofia 模块的启动加载与 SIP 中继的双向握手细节mod_sofia是 FreeSWITCH 的 SIP 协议栈心脏它的加载时机、参数绑定、状态同步直接决定呼入呼出是否“有信令无媒体”或“能注册不能呼出”。V1.2.7 版本中sofia的行为与新版有显著差异必须按其原始设计来配置。3.1 mod_sofia 的加载流程从 modules.conf 到 NUA 实例的完整初始化回顾项目正文中的fs-m-sofia-0001描述mod_sofia的加载不是简单的dlopen而是一系列强依赖的初始化模块加载入口switch_loadable_module_init()遍历conf/autoload_configs/modules.conf发现load modulemod_sofia/后调用switch_loadable_module_load_module_ex()符号解析switch_dso_data_sym(dso, mod_sofia_module_interface, derr)查找mod_sofia.c中定义的switch_module_interface_t结构体其中包含load,runtime,shutdown三个函数指针load 函数执行mod_sofia_load()被调用它执行初始化sofia_globals全局结构体调用nua_create()创建 Nokia Sofia 的 NUANUA: Network User Agent实例加载conf/sip_profiles/下所有 profile XML 文件为每个 profile 创建一个sofia_profile_t对象为每个 profile 的 gateway 创建nua_handle_t并调用nua_register()发起注册请求如果registertrueruntime 线程启动switch_loadable_module_runtime()为mod_sofia创建专用线程该线程循环调用nua_step()处理 SIP 事件如nua_r_register,nua_i_invite。提示这就是为什么sofia的日志级别要设为DEBUG才能看到nua_r_register 200 OK—— 因为注册响应是由nua_step()在 runtime 线程中解析的不是主线程处理的。在conf/autoload_configs/sofia.conf.xml中设置param namelog-level value5/5DEBUG。3.2 SIP 中继的双向握手对等模式peer下的 INVITE 构造与响应处理项目摘要中强调“外呼用的是对等中继模式”这意味着你的 FreeSWITCH 不向远端注册而是作为 SIP UAC 直接向远端 UAS 发起 INVITE。这种模式下sofia的行为与注册模式完全不同INVITE 构造bridge sofia/external/gw_unicom/13800138000时sofia不使用 gateway 的from-user作为From:头而是使用profile的rtp-ip和sip-port构造Contact:头并将To:头设为sip:13800138000203.0.113.100:5060远端中继地址SDP Offer 生成sofia会根据gateway配置中的param nameext-rtp-ip value.../和param nameext-sip-ip value.../生成 SDP 的c和o行确保 NAT 穿透180 Ringing 处理远端返回180 Ringing后sofia触发SWITCH_EVENT_CHANNEL_PROGRESS事件dialplan中若定义了progress状态的 extension可执行playback播放回铃音200 OK 处理远端返回200 OK并携带 SDP Answer 后sofia解析 SDP建立 RTP 流并触发SWITCH_EVENT_CHANNEL_ANSWER此时dialplan的answer状态 extension 才开始执行。下面是一个典型的对等中继 gateway 配置重点看registerfalse和proxy的用法!-- conf/sip_profiles/external/gw_peer.xml -- gateway namegw_peer param nameusername valuepeeruser/ param namepassword valuepeerpwd/ param namerealm valuesip.peer.example/ param namefrom-user valuepeeruser/ param nameproxy valuesip.peer.example:5060/ !-- 关键指定远端中继地址 -- param nameregister valuefalse/ !-- 关键禁用注册 -- param nameexpire-seconds value3600/ param nameretry-seconds value60/ param namecontext valuepublic/ param nameextension value1000/ !-- 用于 REFER 时的默认扩展 -- param namecaller-id-in-from valuetrue/ param namesupersede valuetrue/ !-- 编解码优先级按远端支持情况调整 -- param nameextension valuePCMA,PCMU,G729/ /gateway注意proxy参数在此模式下是强制的它告诉sofia“所有发给此 gateway 的 INVITE 都要发往这个地址”而不是尝试 DNS SRV 查询。如果远端中继要求 TLS需将proxy改为sips://sip.peer.example:5061并在 profile 中启用 TLS。3.3 呼入路由的深度实现基于 SIP 头字段的精细化路由仅靠network_addr和destination_number做呼入路由太粗糙。真实场景中上游中继会在P-Asserted-Identity、X-Original-Destination、User-Agent等头中携带关键信息。V1.2.7 的sofia支持将任意 SIP 头映射为 channel 变量这是实现高级路由的基础。在conf/sip_profiles/external.xml的settings块中添加param namemanage-presence valuetrue/ param namerfc2833-pt value101/ !-- 将自定义头映射为变量 -- param namesip-headers valueP-Asserted-Identity,X-Original-Destination,User-Agent/ !-- 启用头字段解析 -- param nameenable-100rel valuetrue/然后在 dialplan 中即可读取!-- conf/dialplan/public.xml -- extension nameadvanced_inbound_routing condition fieldsip_h_P-Asserted-Identity expression^quot;(.)quot;lt;sip:(.)gt;$ action applicationset dataasserted_name$1/ action applicationset dataasserted_number$2/ /condition condition fieldsip_h_X-Original-Destination expression^(\?1[2-9]\d{9})$ action applicationset dataoriginal_dest$1/ /condition condition fieldsip_h_User-Agent expression^Asterisk.*$ action applicationset datasource_typeasterisk_trunk/ /condition condition fieldsip_h_User-Agent expression^Grandstream.*$ action applicationset datasource_typegrandstream_phone/ /condition /extension extension nameroute_by_asserted condition field${asserted_number} expression^138\d{8}$ action applicationtransfer datasales${domain} XML default/ /condition condition field${original_dest} expression^186\d{8}$ action applicationtransfer datatechsupport${domain} XML default/ /condition condition field${source_type} expressionasterisk_trunk action applicationset datacall_priorityhigh/ action applicationtransfer dataqueue:priority_queue${domain} XML default/ /condition /extension这段配置展示了 V1.2.7 的强大之处它允许你在呼入的第一时间就从 SIP 头中提取出上游系统的真实意图而非依赖不可靠的From:头。比如上游 Asterisk 中继可能把主叫号码放在P-Asserted-Identity中而把网关自己的号码放在From:中这时sip_h_P-Asserted-Identity就是唯一可信来源。3.4 外呼路由的深度实现基于 codec 协商结果的动态 fallback外呼时远端中继可能不支持你首选的 codec如 G.729导致通话无声。V1.2.7 提供了codec_negotiation机制可在bridge失败后自动 fallback 到次选 codec。首先在 gateway 配置中声明多 codec 支持!-- conf/sip_profiles/external/gw_fallback.xml -- gateway namegw_fallback param nameusername valuefb123/ param namepassword valuefb456/ param namerealm valuesip.fallback.example/ param namefrom-user valuefb123/ param nameproxy valuesip.fallback.example:5060/ param nameregister valuefalse/ !-- 按优先级排序 -- param nameextension valueG729,PCMA,PCMU,OPUS/ !-- 启用协商 -- param nameinbound-codec-negotiation valuetrue/ param namedisable-transcoding valuefalse/ /gateway然后在 dialplan 中捕获协商失败事件!-- conf/dialplan/public.xml -- context namepublic extension nameoutbound_with_fallback condition fielddestination_number expression^1[3-9]\d{9}$ action applicationset dataorigination_codec_stringG729,PCMA,PCMU/ action applicationset datafallback_codec_stringPCMU,PCMA/ action applicationbridge datasofia/external/gw_fallback/$1/ /condition /extension !-- 当 bridge 失败时触发 -- extension namehandle_bridge_failure condition field${bridge_result} expression^FAILURE$ action applicationlog dataINFO Bridge failed, trying fallback codec/ action applicationset dataorigination_codec_string${fallback_codec_string}/ action applicationbridge datasofia/external/gw_fallback/$1/ /condition /extension /context这里的关键是origination_codec_string变量它会被sofia模块读取并用于构造 SDP Offer。第一次bridge使用G729若远端回复488 Not Acceptable Heresofia会将bridge_result设为FAILURE从而触发 fallback extension。这种机制比单纯在 gateway 中列一堆 codec 更可控因为你可以在 dialplan 中加入日志、告警、甚至切换到另一个 gateway。4. 避坑指南FreeSWITCH V1.2.7 呼入呼出路由配置的 5 个高频翻车点FreeSWITCH V1.2.7 是个成熟但“有脾气”的版本。很多配置在文档里看着没问题一上线就出状况。以下是我在多个模拟项目X中踩过的、最痛的 5 个坑按现象、原因、解决三步给出答案全是血泪经验。4.1 现象呼入电话能振铃但摘机后无音频Wireshark 显示 RTP 流发往错误 IP原因sofiaprofile 中rtp-ip或sip-ip配置为auto而服务器有多个网卡如 eth0 公网、eth1 内网auto模式下switch_find_local_ip()会随机选择一个网卡的 IP导致 SDP 中的c行 IP 错误。解决在conf/sip_profiles/external.xml中将param namertp-ip valueauto/改为param namertp-ip value203.0.113.100/填你对外的公网 IP同时设置param namesip-ip value203.0.113.100/如果服务器在 NAT 后还需设置param nameext-rtp-ip value203.0.113.100/和param nameext-sip-ip value203.0.113.100/确保 SDP 中的c和o行都指向公网 IP执行sofia profile external restart生效不要只reloadxml。4.2 现象外呼时bridge命令返回ERR NO ROUTE TO EXTENSION但 gateway 状态显示REGED原因bridge语法中 gateway 名称拼写错误或 gateway 文件未被sofia加载。V1.2.7 的sofia在启动时只加载conf/sip_profiles/external/目录下以.xml结尾的文件如果文件名是gw_unicom.conf它会被忽略。解决运行sofia status profile external检查输出中Gateways部分是否列出你的 gateway 名称如果没有检查文件名是否为.xml且文件权限为644运行sofia xmlstatus查看是否有 XML 解析错误确保conf/sip_profiles/external.xml中gateways块存在且未被注释V1.2.7 默认有手动执行sofia profile external rescan强制重新扫描 gateway 目录。4.3 现象呼入时dialplan中的set变量在bridge中无法读取log显示变量为空原因set动作放在了错误的state钩子中。例如在routing状态的 extension 中set了一个变量但bridge应用是在execute状态执行的而execute状态的 dialplan 无法读取routing状态设置的变量它们作用域不同。解决所有为bridge准备的变量必须在new或init状态的 extension 中set在conf/dialplan/public.xml中确认new状态的 extension 存在且breakon-true使用eval应用验证变量action applicationeval data${my_var}/如果输出为空说明变量未在正确时机设置万不得已时用export替代setaction applicationexport datamy_varvalue/export的变量作用域更广。4.4 现象启用 TLS 后呼入呼出全部失败sofia日志显示SSL_accept error原因V1.2.7 的 OpenSSL 依赖较老不支持 TLS 1.2 以上版本而现代中继默认禁用 TLS 1.0/1.1。同时证书链不完整缺少 intermediate CA也会导致握手失败。解决在conf/sip_profiles/external.xml中添加param nametls-version valuetlsv1/强制 TLS 1.0生成证书时确保fullchain.pem包含 server cert intermediate CAprivkey.pem是私钥在conf/autoload_configs/sofia.conf.xml中设置param nametls-ciphers valueDEFAULT:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA/用openssl s_client -connect sip.peer.example:5061 -tls1测试 TLS 握手是否成功。4.5 现象dialplan中transfer到另一个 context 后被转移的呼叫无法听到 IVR 提示音直接挂断原因transfer应用会终止当前 channel 的状态机并创建一个新 channel。如果目标 context 中没有为new状态定义 extension新 channel 会因无指令而立即 hangup。解决确保被transfer的目标 context如sales.xml中有namenew的 extension该 extension 至少包含一个answer或playback应用例如extension namenew condition fielddestination_number expression^.*$ action applicationanswer/ action applicationplayback dataivr/sales_greeting.wav/ /condition /extension检查conf/dialplan/目录下目标 context 文件是否被autoload_configs/dialplan.conf.xml加载V1.2.7 默认加载所有.xml文件。5. 验收与答辩用四步法验证路由配置的完备性与鲁棒性交毕设或课设光有配置文件不够评审老师要看你是否真正吃透了这套方案。我带过几个模拟项目X发现最有效的验收不是跑通一个用例而是用一套结构化方法覆盖“正常、边界、异常、灾备”四个维度。下面是我自己总结的四步法每一步都对应一个可执行的 CLI 命令和一个必查的日志片段答辩时直接打开终端演示比讲 PPT 有力得多。5.1 第一步验证呼入路由的三层鉴权正常边界目标证明你能区分内网、DMZ、WebRTC 三种来源并执行不同路由。操作步骤用sofiaCLI 模拟三种呼入# 模拟内网呼入192.168.10.100 fs_cli -x originate {origination_caller_id_number1001,origination_caller_id_nameInternal}sofia/internal/1002 echo() # 模拟 DMZ 中继呼入203.0.113.50 fs_cli -x originate {origination_caller_id_number9001,origination_caller_id_nameTrunk}sofia/external/9002 echo() # 模拟 WebRTC 呼入需先用 browser 连接 fs_cli -x originate {origination_caller_id_number8001,origination_caller_id_nameWebRTC}sofia/external/8002 echo()检查fs_cli输出中的Context:字段确认内网呼入显示Context: internalDMZ 呼入显示Context: trunkWebRTC 呼入显示Context: webrtc查看logs/freeswitch.log搜索tag_inbound_source确认每条呼入都命中了正确的condition且inbound_source变量被正确设置。提示originate命令中的sofia/internal/...是模拟内网 profilesofia/external/...是模拟外部 profile。V1.2.7 的originate支持直接指定 profile这是验证路由最干净的方式不用真打电话。5.2 第二步验证外呼路由的动态 gateway 选择正常边界目标证明你能根据被叫号码前缀自动选择gw_mobile或gw_unicom且 fallback 机制有效。操作步骤执行两次外呼本文还有配套的精品资源点击获取