简介:这份文档面向使用 Freeswitch 搭建 VoIP 通信平台的运维与开发人员,聚焦呼入呼出路由配置这一关键环节,帮助读者理清拨号计划、SIP 中继与对等中继模式下的外呼实现思路。资源包内共 1 个 doc 文件,约 221KB,内容围绕 Freeswitch 系统架构展开,涵盖程序启动流程、消息分发机制、mod_sofia 模块组成与启动过程等核心知识点,并配有术语定义与条目编号规范,便于按模块查阅。文档以个人学习总结的形式梳理整体架构,先讲 CORE 再讲 MODULE,适合作为理解 Freeswitch 内部运行机制的参考笔记。目前已有 6455 人学习下载,对需要配置内呼外呼、设置 SIP 中继及排查路由问题的读者具有一定参考价值。
1. 从一通打不通的外呼说起:FreeSWITCH 路由配置到底卡在哪
很多团队第一次把 FreeSWITCH 接上 SIP 网关时,都会遇到一个很典型的现象:内部分机互拨一切正常,但一拨外线就静默失败,日志里只有一句NORMAL_CLEARING,看不出任何有用信息。问题往往不在网关,也不在运营商,而是出在dialplan和sip_profiles这两层配置没有对齐——呼入不知道该落到哪个 context,呼出不知道该从哪个 gateway 出去。这份 FreeSWITCH 路由配置资料围绕的正是这个环节:它把呼入路由、呼出路由、SIP 中继(对等中继模式)三件事串成一条可复现的链路,适合已经能把 FreeSWITCH 跑起来、但卡在“内外网打通”这一步的运维和通信开发人员。下面按“先看清结构、再动手配、最后排坑”的顺序拆开讲,每一步都落到具体文件和参数上。
2. 先理清 FreeSWITCH 的模块与事件骨架:路由为什么长这样
在改任何一行 XML 之前,得先明白 FreeSWITCH 为什么把路由拆成拨号计划、端点、中继三块。这不是设计者故意绕弯,而是它的核心设计决定的:核心只提供通用函数和状态机,具体协议能力全部由 module 挂进来。你配的每一条路由,最终都是通过某个 module 的接口被调用的。
2.1 核心与模块的分层:路由配置的落点在哪
FreeSWITCH 启动时,src/switch.c里的main()先解析启动参数、给全局目录变量SWITCH_GLOBAL_dirs赋值、创建 pid 文件,然后调用switch_core_init_and_modload()。这个函数做两件事:先switch_core_init()初始化核心(APR、会话、控制台、事件、XML、日志、switch.conf、状态机、SQLite 线程、定时任务、RTP),再switch_loadable_module_init()按conf/modules.conf和conf/post_load_modules.conf逐个加载模块。每个模块被加载时,通过switch_loadable_module_load_module_ex()找到动态库、定位#name#_module_interface入口结构体、执行SWITCH_MODULE_LOAD_FUNCTION。
这套机制对路由配置的直接含义是:你写的 dialplan 能不能生效,取决于对应 endpoint 模块有没有被加载。比如mod_sofia没在modules.conf里,sip_profiles下的 profile 根本不会起来,呼入呼出都无从谈起。所以排查路由问题的第一步永远是确认模块加载状态,而不是盯着 XML 找语法错误。
常见做法是先看模块清单,再确认 sofia 是否在运行:
# 进入 fs_cli 后执行,确认 sofia 端点已加载 fs_cli -x "module_exists mod_sofia" # 列出当前所有 sofia profile 及其状态 fs_cli -x "sofia status" # 查看已注册的网关 fs_cli -x "sofia status gateway"module_exists返回true说明模块在;sofia status会列出每个 profile 的RUNNING状态和绑定的 IP:端口,这是后面配中继时的对照基准。如果这里就是空的,先回去检查modules.conf,别往下走。
2.2 事件与状态机:呼入呼出在内部是怎么被驱动的
FreeSWITCH 每个信道有自己的线程,线程通过状态机与核心交互,状态变化会以事件形式抛到EVENT_DISPATCH_QUEUE队列,由分发线程消费。事件类型定义在src/include/switch_types.h,和路由强相关的有SWITCH_EVENT_CHANNEL_CREATE、SWITCH_EVENT_CHANNEL_ANSWER、SWITCH_EVENT_CHANNEL_BRIDGE、SWITCH_EVENT_CHANNEL_HANGUP、SWITCH_EVENT_CHANNEL_EXECUTE等。模块通过switch_event_bind()把回调绑到某类事件上,switch_event_unbind()解绑。
理解这一层对排障的价值在于:呼入进来后走哪个 context、执行了哪个 application,都能从事件流里看到。当一通外呼失败时,你不需要猜,直接订阅事件就能看到信道创建、执行、挂断的完整轨迹。我一般排路由问题会先开一个事件监听窗口:
# 订阅信道相关事件,观察一通呼叫的完整生命周期 fs_cli -x "event plain CHANNEL_CREATE CHANNEL_ANSWER CHANNEL_EXECUTE CHANNEL_HANGUP CHANNEL_HANGUP_COMPLETE"订阅后拨一通电话,终端会逐条打印事件。重点看CHANNEL_EXECUTE里Application字段是不是你预期的bridge或transfer,以及CHANNEL_HANGUP_COMPLETE里的Hangup-Cause。如果Application是空的或者停在playback,说明 dialplan 匹配到了错误的 extension;如果Hangup-Cause是CALL_REJECTED,问题多半在网关侧或认证。这一步能把“玄学不通”压缩成一个具体字段。
2.3 拨号计划、端点、中继三者的职责边界
把职责分清,配置才不会互相打架:
| 组件 | 配置文件位置 | 负责什么 | 配错的典型症状 |
|---|---|---|---|
| Dialplan | conf/dialplan/*.xml | 按被叫号码匹配 extension,决定执行哪个 application | 呼入落到默认 context,直接挂断 |
| SIP Profile | conf/sip_profiles/*.xml | 定义 FreeSWITCH 监听哪个 IP:端口、用 UDP/TCP/TLS、绑定哪个 context | 外部注册不上,sofia status里 profile 不 RUNNING |
| Gateway | conf/sip_profiles/*.xml内<gateway> | 定义出局对端地址、认证、注册策略 | 外呼无响应或 407 认证失败 |
呼入链路是:外部请求打到某个 profile 的端口 → profile 把呼叫送进它context属性指定的 dialplan context → dialplan 匹配 extension → 执行 application(如transfer到分机或bridge到网关)。呼出链路是:内部分机拨号 → dialplan 匹配到外呼前缀 → 执行bridge sofia/gateway/<网关名>/<号码>→ 从对应 gateway 出去。两条链路共用 dialplan,但入口和出口不同,这就是为什么“内呼通、外呼不通”几乎总是 gateway 或外呼 extension 的问题。
3. 呼入路由配置:从 profile 的 context 到 dialplan 的 extension
呼入配置的核心就一句话:让外部呼叫准确落进你写好的 context,并被正确的 extension 接住。听起来简单,但 profile 的context属性、dialplan 的context名、extension 的destination_number三者任何一处对不上,呼叫就会掉进默认逻辑被静默处理。
3.1 配置 SIP Profile 的 context 与端口
先看 profile。一个典型的 external profile 片段如下,重点是context、sip-port、sip-ip三个属性:
<!-- conf/sip_profiles/external.xml 关键片段 --> <profile name="external"> <!-- 监听端口,对等中继模式下对端要往这个端口发 INVITE --> <param name="sip-port" value="5080"/> <!-- 本机对外 IP,多网卡时务必写实际可达的那个 --> <param name="sip-ip" value="192.0.2.10"/> <!-- 呼入落到哪个 dialplan context,必须与 dialplan 文件名/context 名一致 --> <param name="context" value="public"/> <!-- 关闭认证,对等中继通常按 IP 信任;需要认证时改为 true 并配 directory --> <param name="accept-blind-reg" value="false"/> <param name="auth-calls" value="false"/> </profile>context="public"意味着从 5080 端口进来的呼叫会去conf/dialplan/public.xml里找匹配。sip-ip写错是多网卡环境最常见的翻车点:机器上有内网和公网两块网卡时,如果这里写了内网地址,对端发到公网地址的 INVITE 就到不了监听 socket。auth-calls在对等中继场景下一般关掉,靠 IP 白名单信任对端;如果对端要求注册认证,则要打开并在 directory 里建用户。
改完 profile 必须 reload 才生效,且 reload 会短暂中断该 profile 上的呼叫:
# 重载指定 profile,观察返回是否 OK fs_cli -x "sofia profile external restart" # 确认 profile 状态和监听地址 fs_cli -x "sofia status profile external"restart比reload更彻底,会重建 socket。返回里重点看RUNNING和Listen IP,如果Listen IP不是你写的sip-ip,说明配置没被读到或者被别的 profile 占了端口。
3.2 在 public context 里写呼入 extension
profile 把呼叫送进publiccontext 后,dialplan 要能接住。下面这段把任意被叫号码转到一个内部分机,同时保留原始主叫信息:
<!-- conf/dialplan/public.xml --> <include> <context name="public"> <extension name="inbound_to_1001"> <!-- 匹配任意被叫号码,实际部署建议按 DID 精确匹配 --> <condition field="destination_number" expression="^(\d+)$"> <!-- 设置主叫显示,便于分机识别来源 --> <action application="set" data="caller_id_name=Inbound-${caller_id_number}"/> <!-- 转到内部 context 的分机 1001 --> <action application="transfer" data="1001 XML default"/> </condition> </extension> </context> </include>expression="^(\d+)$"匹配纯数字被叫,$1可在后续 action 里引用。transfer的data格式是<目标> <dialplan类型> <context>,这里把呼叫从public转到defaultcontext 的 1001 分机。生产环境不建议用通配,应该按实际 DID 写精确正则,否则任何打到这个端口的呼叫都会被转走,等于把入口敞开。
改完 dialplan 用reloadxml热加载,不需要重启服务:
fs_cli -x "reloadxml" # 验证 extension 是否被解析到 fs_cli -x "xml_locate dialplan public"xml_locate会打印该 context 解析后的完整 XML,如果里面没有你刚写的 extension,说明文件没被 include 或者 XML 语法有误(比如标签未闭合),这时看控制台日志里的 XML 解析报错。
3.3 呼入链路验证:用事件和日志确认落点
配置写完不要靠“拨一下试试”这种黑匣子方式验证。正确做法是订阅事件 + 看日志双管齐下。拨入一通电话,事件流里应该依次出现CHANNEL_CREATE(方向inbound)、CHANNEL_EXECUTE(Application: transfer)、CHANNEL_BRIDGE(如果分机接了)。如果CHANNEL_CREATE都没出现,说明呼叫根本没到 FreeSWITCH,问题在网关或网络层;如果出现了但Application不是transfer,说明 dialplan 匹配错了 extension。
日志侧把 sofia 的日志级别临时调高,能看到 SIP 报文交互:
# 提高 sofia 日志级别,观察 INVITE 的接收与应答 fs_cli -x "sofia loglevel all 9" # 排查完记得调回,避免日志刷屏 fs_cli -x "sofia loglevel all 0"级别 9 会打印完整 SIP 消息。重点看 FreeSWITCH 有没有回100 Trying和200 OK,以及Contact头里的地址是不是可达。如果只看到 INVITE 没有应答,回到 3.1 检查 profile 监听地址。
4. 呼出路由与对等中继:gateway 配置与外呼 extension
呼出比呼入多一层:除了 dialplan 匹配,还要有一个能出去的 gateway。对等中继模式下,gateway 指向对端设备,FreeSWITCH 作为对等节点直接发 INVITE,不经过注册。
4.1 对等中继的 gateway 参数怎么填
gateway 写在 profile 文件里,关键参数是proxy、register、realm:
<!-- conf/sip_profiles/external.xml 内,与 profile 同级 --> <gateway name="peer_gw"> <!-- 对端设备地址和端口,对等中继填对端实际监听地址 --> <param name="proxy" value="198.51.100.20:5060"/> <!-- 对等中继不注册,设为 false;需要注册时改 true 并配 username/password --> <param name="register" value="false"/> <!-- 认证域,对端要求认证时填写 --> <param name="realm" value="198.51.100.20"/> <param name="username" value="fs_out"/> <param name="password" value="******"/> <!-- 外呼时主叫号码,按对端要求填写 --> <param name="caller-id-in-from" value="true"/> </gateway>proxy是对端 SIP 地址,对等中继下这就是唯一必须填对的参数。register=false表示不向对端注册,直接按 IP 发呼叫;如果对端是运营商要求注册,则改true并填username/password。caller-id-in-from=true把主叫号码放进From头,很多网关只认From里的号码做鉴权,这个参数不设会导致外呼被拒但日志里看不出原因。
gateway 改完同样要 restart profile,然后用sofia status gateway确认状态是NOREG(未注册,对等模式正常)或REGED(已注册):
fs_cli -x "sofia profile external restart" fs_cli -x "sofia status gateway peer_gw"如果状态是FAILED或UNREGED,看返回里的State和Status字段,通常是proxy地址不可达或认证信息错误。
4.2 外呼 extension:前缀匹配与 bridge 到 gateway
外呼 dialplan 一般写在defaultcontext,按拨号前缀匹配,然后bridge到 gateway:
<!-- conf/dialplan/default.xml 内 --> <extension name="outbound_via_peer"> <!-- 匹配 0 开头的外线号码,去掉前缀 0 后作为被叫 --> <condition field="destination_number" expression="^0(\d{7,15})$"> <!-- 设置主叫号码,按对端要求调整 --> <action application="set" data="effective_caller_id_number=1001"/> <!-- 从 peer_gw 发出,$1 是去掉前缀后的真实号码 --> <action application="bridge" data="sofia/gateway/peer_gw/$1"/> </condition> </extension>expression="^0(\d{7,15})$"表示以 0 开头、后跟 7 到 15 位数字,$1捕获去掉 0 后的号码。bridge的data格式是sofia/gateway/<网关名>/<被叫>,网关名必须和 gateway 的name属性完全一致。这里最容易踩的坑是前缀没去干净:如果直接bridge .../$0,对端收到的是带 0 的号码,会被判为空号。
改完reloadxml,然后拨一通外线,用事件流确认CHANNEL_EXECUTE里Application: bridge、Application-Data是sofia/gateway/peer_gw/xxx。如果Application-Data里的号码不对,回去检查正则捕获组。
4.3 外呼失败的三层定位法
外呼不通时按三层往下切,比盲目改配置高效得多:
第一层看 dialplan 有没有匹配。事件流里如果没有CHANNEL_EXECUTE的bridge,说明正则没匹配上,用xml_locate dialplan default确认 extension 在不在。
第二层看 gateway 有没有发出去。sofia loglevel all 9下如果看到 FreeSWITCH 发出了 INVITE 但没收到100 Trying,问题在对端地址或网络;如果收到407 Proxy Authentication Required,是认证参数不对,回去核对username/password/realm。
第三层看对端返回码。CHANNEL_HANGUP_COMPLETE里的Hangup-Cause如果是CALL_REJECTED,多半是对端策略拒绝(主叫号码不合法、被叫不在白名单);如果是NO_ROUTE_DESTINATION,是对端没有到该号码的路由。这三层走完,问题基本能定位到具体参数。
5. 避坑与排查:五个真实踩过的坑
路由配置的坑大多不是语法错误,而是“配置对了但环境不对”。下面五条是我在实际部署里反复遇到的,按现象、原因、解决写清楚。
坑一:profile 显示 RUNNING,但外部呼叫进不来。现象是sofia status一切正常,对端却收不到应答。原因通常是sip-ip写了内网地址,而对端往公网地址发 INVITE,socket 没绑在那个地址上。解决是把sip-ip改成对端实际可达的地址,restart profile 后用sofia status profile external确认Listen IP一致。多网卡机器上这个坑几乎必踩。
坑二:外呼日志显示 bridge 成功,但对端说没收到呼叫。现象是CHANNEL_EXECUTE里bridge执行了,Hangup-Cause却是NORMAL_CLEARING。原因是 gateway 的proxy地址写成了域名但 DNS 解析到了错误 IP,或者端口写错(对端监听 5060 你写了 5080)。解决是用sofia status gateway peer_gw看解析后的地址,必要时在/etc/hosts里固定映射,端口以对端实际配置为准。
坑三:呼入能通,但主叫号码显示为 unknown。现象是分机接起来看不到来电号码。原因是 profile 里没开caller-id-in-from,或者对端把号码放在P-Asserted-Identity头里而 FreeSWITCH 没解析。解决是在 profile 里加<param name="caller-id-in-from" value="true"/>,并在 dialplan 里用${caller_id_number}显式设置显示名。
坑四:reloadxml 后新 extension 不生效。现象是文件改了、reloadxml返回成功,但呼叫还是走旧逻辑。原因是 XML 文件没被主 dialplan 文件 include,或者文件里有未闭合标签导致整个文件解析失败被跳过。解决是xml_locate dialplan <context>看解析结果里有没有新 extension,没有就查控制台日志的 XML 报错,逐个文件用xmllint校验。
坑五:对等中继下外呼被 403 拒绝。现象是 INVITE 发出去收到403 Forbidden。原因是对端按 IP 做信任但你的出口 IP 和对端白名单不一致(比如经过 NAT 后源地址变了),或者From头里的主叫号码不在对端允许范围。解决是确认出口源 IP,必要时在对端加白;主叫号码按对端要求用effective_caller_id_number设置成合法号段。
提示:每次改完 profile 或 gateway,先
restart再验证;改完 dialplan,先reloadxml再xml_locate确认解析结果。跳过确认直接拨号,等于把排障成本翻倍。
6. 进阶:用事件订阅和日志级别把路由问题变成可观测数据
路由配置做到能通只是及格线,真正省时间的是把它变成可观测的。我现在的习惯是:任何一次路由变更,都先用事件订阅跑一遍完整呼叫,把CHANNEL_CREATE到CHANNEL_HANGUP_COMPLETE的事件序列存下来,作为这条链路的“基线”。以后出问题,拿新的事件序列和基线对比,差异点就是故障点。
具体做法是开一个长期的事件监听,把输出重定向到文件,按时间切分:
# 后台订阅关键事件,输出带时间戳,便于事后比对 fs_cli -x "event plain CHANNEL_CREATE CHANNEL_EXECUTE CHANNEL_BRIDGE CHANNEL_HANGUP_COMPLETE" \ | while read -r line; do echo "$(date '+%F %T') $line"; done >> /var/log/fs_route_trace.log这条命令把事件流按行加时间戳落盘。正常呼叫的基线里,CHANNEL_CREATE的Call-Direction应该是inbound或outbound,CHANNEL_EXECUTE的Application应该是transfer或bridge,CHANNEL_HANGUP_COMPLETE的Hangup-Cause应该是NORMAL_CLEARING。任何一项偏离,直接对应到第 3、4 章的具体配置点。
配合日志级别做分层排查也很实用。日常把 sofia 日志压到 0,只在排障时临时开到 9,并且用fs_cli的过滤功能只看某个呼叫的 UUID:
# 只跟踪指定呼叫 UUID 的日志,避免全量刷屏 fs_cli -x "console loglevel debug" fs_cli -x "uuid_setvar <call_uuid> loglevel debug"uuid_setvar给单个信道设日志级别,比全局调级别精准得多,生产环境排障时不会影响其他呼叫。这个技巧在并发量大的场景下几乎是必备的,全局开 debug 会把磁盘写满。
还有一个容易被忽略的点:switch_core_init()里switch_find_local_ip获取本地 IP 时会访问外部地址,如果机器网络受限,这一步可能超时拖慢启动。启动慢但最终能起来的情况,先查这个。另外EVENT_DISPATCH_QUEUE的容量和 CPU 个数相关,高并发下如果事件消费不及时,可以适当调大分发线程数,但这属于性能调优范畴,路由配置阶段先保证事件能正常消费即可。
从那以后我每次改完路由配置,都强制走一遍“restart → reloadxml → xml_locate → 事件订阅拨测”这四步,不跳步。这套流程看着笨,但把“改完不知道对不对”变成了“每一步都有明确输出”,排障时间从小时级压到分钟级。希望帮到你。
本文还有配套的精品资源,点击获取