简介:本资源是一份面向VoIP通信工程师与FreeSWITCH初学者的实战配置指南,聚焦呼入呼出路由核心场景,系统解决SIP中继对接、对等模式外呼、内网呼叫分发等典型问题。内容涵盖FreeSWITCH V1.2.7架构解析、XML拨号计划编写逻辑、sip_profiles中继参数配置(含IP/端口/传输协议/认证设置)、安全加固与错误恢复策略等关键实践要点,特别适合需快速部署企业级语音中继或调试网关互通的技术人员。资源为单文件DOC文档,共17页,大小221KB,结构清晰,含引言、组件架构、CORE启动流程、MOD_SOFIA模块详解及参考目录,便于按模块查阅与实操对照。目前已有6451人学习下载,提供从原理理解到配置落地的完整闭环,是少有的兼顾协议基础、配置细节与排错提示的轻量级专项参考资料。
1. Freeswitch呼入呼出路由配置详解:不是写几行XML就完事,而是要让每通呼入不丢、每路外呼可溯、每个中继不哑火
你刚配好 FreeSWITCH,sip_profiles 下的 external.xml 也 reload 了,但呼入电话一来就 404 Not Found,外呼拨出去直接 503 Service Unavailable;或者更玄学的是——白天全链路正常,凌晨三点突然所有外呼失败,日志里只有一句sofia: sofia/external is down,查防火墙、查网关、查 DNS 全都没问题。这不是玄学,是路由逻辑没闭环。本文讲的不是“FreeSWITCH 怎么安装”,而是聚焦在真实生产环境里最常翻车的呼入呼出路由环节:从 SIP 中继怎么填才不被对端拒绝,到 dialplan 里<condition field="destination_number" expression="^1[3-9]\d{9}$">这种正则为什么在某些 profile 下根本匹配不到;从bridge应用里sofia/gateway/myisp/13800138000的 gateway 名称必须和sip_profiles/external/myisp.xml文件名严格一致,到originate命令里漏写&ignore_early_media=true导致媒体流卡在 early 183 而无法建立通话。它适合正在调试 SIP 中继对接的某公司 VoIP 工程师、接手遗留 FreeSWITCH 系统的某高校实验室运维、或准备用 FreeSWITCH 搭建多租户呼叫中心的某跨平台系统开发者——你不需要从零造轮子,但必须知道哪颗螺丝松了会整机停摆。
2. 呼入路由落地:SIP Profile 是入口守门员,dialplan 是决策大脑,二者错位一毫,呼入即失
FreeSWITCH 的呼入不是“电话打进来就自动进 dialplan”,而是一条严格分段的流水线:SIP 协议栈收包 → Profile 解析 From/To/Contact → Channel 创建 → Dialplan 匹配 context → 执行 application。任何一环断开,呼入就静音。下面拆解真实可复现的配置链路,全部基于 V1.2.7 源码结构与默认 conf 目录布局(非 Docker 镜像路径)。
2.1 SIP Profile 配置:external.xml 不是模板,是你的对外身份证
呼入流量首先进入conf/sip_profiles/external.xml。很多人直接复制官网示例,却忽略三个致命字段:<param name="rtp-ip" value="$${local_ip_v4}"/>、<param name="sip-ip" value="$${local_ip_v4}"/>和<param name="ext-rtp-ip" value="auto-nat"/>。它们不是可选项,而是决定呼入能否穿透 NAT 的关键。
<!-- conf/sip_profiles/external.xml --> <configuration name="sip_profile" description="External SIP Profile"> <settings> <param name="rtp-ip" value="192.168.1.100"/> <param name="sip-ip" value="192.168.1.100"/> <param name="ext-rtp-ip" value="203.0.113.45"/> <param name="ext-sip-ip" value="203.0.113.45"/> <param name="rtp-port-range" value="16384-20000"/> <param name="inbound-codec-prefs" value="PCMU,PCMA,G722,OPUS"/> </settings> <gateways> <!-- 此处不填gateway,呼入走profile直连,非中继模式 --> </gateways> </configuration>参数说明:
rtp-ip/sip-ip:必须填 FreeSWITCH 所在服务器的内网真实 IP(如192.168.1.100),不能写auto或0.0.0.0,否则 Sofia 栈无法绑定端口;ext-rtp-ip/ext-sip-ip:必须填你暴露给公网的NAT 映射 IP(如云服务器的弹性公网 IP203.0.113.45),这是告诉远端设备:“我的媒体和信令实际从这个 IP 来”;rtp-port-range:务必避开系统保留端口(<1024)和常用服务端口(如 5060、8080),实测16384-20000是安全区间;inbound-codec-prefs:顺序即优先级,PCMU(G.711u)放首位,避免因编解码协商失败导致 488 Not Acceptable Here。
配置完必须执行fs_cli -x "sofia status profile external",重点核对三行:
Status: UP RTP IP: 192.168.1.100 Ext RTP IP: 203.0.113.45若Status为DOWN,90% 是rtp-ip或sip-ip填错;若Ext RTP IP显示auto-nat,说明未生效,需检查变量$${local_ip_v4}是否在vars.xml中正确定义。
2.2 Dialplan Context 绑定:profile 和 context 必须双向认证,单向匹配等于白配
Profile 收到 SIP INVITE 后,会根据To头中的 SIP URI 提取 domain,再匹配dialplan中<context name="...">的name属性。这不是模糊匹配,而是精确字符串比对。例如:
<!-- conf/dialplan/default.xml --> <context name="public"> <extension name="handle_inbound"> <condition field="destination_number" expression="^1[3-9]\d{9}$"> <action application="log" data="INFO Incoming call to ${destination_number}"/> <action application="bridge" data="user/1001@${domain_name}"/> </condition> </extension> </context>此处context name="public"必须与external.xml中的<param name="context" value="public"/>完全一致。若external.xml里写的是<param name="context" value="default"/>,而 dialplan 只有publiccontext,则所有呼入直接返回404 Not Found,且日志中不会报错,只会静默丢弃。
验证方法:在fs_cli中执行sofia status profile external,找到Context:字段,确认其值与 dialplan 文件中<context name="xxx">的xxx严格相等。常见错误是external.xml里写default,而default.xml里 context 名却是public——这种错位在 reloadxml 后毫无提示,只能靠人工核对。
2.3 呼入号码标准化:别信 From 头,用 regex 重写 destination_number 才可靠
运营商或中继网关发来的呼入 INVITE,To头里的destination_number可能带国家码(+8613800138000)、分机号(1001;ext=101)、甚至非法字符(138-0013-8000)。FreeSWITCH 默认不做清洗,直接拿原始字符串去 dialplan 匹配,必然失败。
正确做法是在 dialplan 最顶层加一个预处理 extension,用set+regex统一规整:
<!-- conf/dialplan/public.xml --> <context name="public"> <!-- 预处理:清洗 destination_number --> <extension name="normalize_number"> <condition field="destination_number" expression="^(\+86|86|0086)(1[3-9]\d{9})$"> <action application="set" data="dialed_extension=$2"/> </condition> <condition field="destination_number" expression="^(1[3-9]\d{9})$"> <action application="set" data="dialed_extension=$1"/> </condition> <condition field="destination_number" expression="^([0-9]{3,4})$"> <action application="set" data="dialed_extension=$1"/> </condition> <condition field="destination_number" expression="^.*$"> <action application="log" data="WARNING Invalid number format: ${destination_number}"/> <action application="hangup" data="UNALLOCATED_NUMBER"/> </condition> </extension> <!-- 主路由逻辑,用清洗后的 dialed_extension --> <extension name="route_to_user"> <condition field="dialed_extension" expression="^1[3-9]\d{9}$"> <action application="bridge" data="user/${dialed_extension}@${domain_name}"/> </condition> </extension> </context>逻辑说明:
- 第一个 condition 匹配带
+86/86/0086前缀的 11 位手机号,提取$2(纯数字)赋给dialed_extension;- 第二个 condition 匹配无前缀的 11 位号,直接赋值;
- 第三个 condition 匹配短号(如
1001),保留原样;- 最后一个兜底 condition,对所有未匹配项记录警告并挂断,避免误入其他 extension;
- 后续所有路由逻辑都基于
dialed_extension,而非原始destination_number,彻底规避格式干扰。
3. 外呼路由落地:Gateway 是信用背书,originate 是执行指令,参数错一位,外呼即中断
外呼不是 dialplan 里写个bridge sofia/gateway/myisp/13800138000就能跑通。它依赖三层强耦合:gateway 配置必须与中继服务商要求完全一致 → originate 命令必须携带必要参数 → dialplan 中的 bridge 必须指向正确 gateway 名称。缺一不可。
3.1 SIP Gateway 配置:myisp.xml 不是名字,是与中继商签的电子合同
conf/sip_profiles/external/myisp.xml是 FreeSWITCH 对接 SIP 中继商的契约文件。它必须包含中继商要求的全部认证、编码、超时参数。以国内某主流 SIP 中继为例(非真实厂商),典型配置如下:
<!-- conf/sip_profiles/external/myisp.xml --> <include> <gateway name="myisp"> <param name="username" value="FS20231001"/> <param name="password" value="aBcDeFgHiJkLmNoPqRsTuVwXyZ"/> <param name="realm" value="sip.myisp.com"/> <param name="from-user" value="FS20231001"/> <param name="from-domain" value="sip.myisp.com"/> <param name="proxy" value="sip.myisp.com:5060"/> <param name="register" value="true"/> <param name="register-transport" value="udp"/> <param name="expire-seconds" value="3600"/> <param name="retry-seconds" value="30"/> <param name="ping" value="30"/> <param name="caller-id-in-from" value="true"/> <param name="extension" value="FS20231001"/> <param name="context" value="public"/> <param name="dtmf-type" value="rfc2833"/> <param name="inbound-codec-prefs" value="PCMU,PCMA,G722"/> <param name="outbound-codec-prefs" value="PCMU,PCMA,G722"/> </gateway> </include>参数说明:
username/password/realm:必须与中继商后台分配的账户逐字一致,大小写敏感;proxy:必须填中继商提供的 SIP 代理地址(如sip.myisp.com:5060),不能写成120.192.100.50:5060(除非明确要求 IP 直连);register:必须为true,FreeSWITCH 需主动向中继商注册,否则中继商不认你为合法 client;register-transport:必须与中继商支持的协议一致,UDP 是默认,若对方强制 TCP 则必须改tcp;outbound-codec-prefs:外呼时优先使用的编解码,必须包含中继商支持的列表,否则协商失败返回488;context:此 gateway 发起的外呼,将进入该 context 的 dialplan,必须与conf/dialplan/public.xml中的<context name="public">对应。
配置后执行fs_cli -x "sofia status gateway myisp",关键字段必须为:
State: REGED Expires: 3599 Ping: 30若State为FAIL或UNREG,立即检查username/password/proxy三要素;若Expires为0,说明注册被拒,大概率是realm或from-domain填错。
3.2 Originate 命令与 Bridge 应用:外呼不是“拨号”,是构造完整 SIP 事务
外呼触发有两种方式:CLI 手动originate或 dialplan 中bridge。无论哪种,都必须显式指定sofia/gateway/myisp/13800138000,其中myisp必须与myisp.xml的<gateway name="myisp">完全一致(包括大小写、下划线)。
CLI 外呼(调试用)
# fs_cli 中执行 originate {origination_caller_id_number=13800138000,origination_caller_id_name="FS-Test"}sofia/gateway/myisp/13800138000 &echo("hello world")参数说明:
{...}内是 channel variables,origination_caller_id_number设定主叫号码,必须是中继商允许的号码(如你购买的 DID 号);sofia/gateway/myisp/13800138000是核心路由串:sofia(协议栈)→gateway(类型)→myisp(gateway 名)→13800138000(被叫);&echo("hello world")是外呼成功后执行的应用,此处为播放语音;- 若省略
{...},主叫 ID 将为空,中继商可能直接拒接。
Dialplan 外呼(生产用)
<!-- conf/dialplan/public.xml --> <context name="public"> <extension name="outbound_call"> <condition field="destination_number" expression="^0\d{10,12}$"> <!-- 国内固话,加 0 前缀 --> <action application="set" data="outbound_number=${destination_number:1}"/> <action application="bridge" data="sofia/gateway/myisp/${outbound_number}"/> </condition> <condition field="destination_number" expression="^1[3-9]\d{9}$"> <!-- 国内手机 --> <action application="bridge" data="sofia/gateway/myisp/${destination_number}"/> </condition> </extension> </context>关键点:
bridge的 data 字段必须是sofia/gateway/{gateway_name}/{number}格式,{gateway_name}不能拼错;- 若中继商要求主叫号码必须为 DID 号,需在
bridge前用set设置origination_caller_id_number,否则外呼失败;bridge后不加&ignore_early_media=true,当对方返回 183 Session Progress 时,FreeSWITCH 会等待媒体流,若对方不发 early media 则超时挂断(现象:外呼拨通但无声)。
3.3 外呼失败排障:看 sip_trace,别猜日志
外呼失败时,freeswitch.log里往往只有Channel [sofia/external/...] hanging up,毫无价值。真正线索在 SIP 信令流:
# 在 fs_cli 中开启信令跟踪 sofia global siptrace on # 然后发起一次外呼 originate sofia/gateway/myisp/13800138000 &echo("test") # 查看实时信令 sofia global siptrace off # 日志会输出完整 INVITE/407/ACK 流程重点关注:
- INVITE 中的
Via、From、To、Contact头是否符合中继商要求; - 是否收到
407 Proxy Authentication Required?若有,说明username/password错; - 是否收到
403 Forbidden?检查realm和from-domain; - 是否收到
486 Busy Here?说明 gateway 注册成功,但中继商侧线路忙; - 是否 INVITE 发出后无任何响应?检查
proxy地址和防火墙 UDP 5060 端口。
4. 呼入呼出联动:Dialplan 是胶水,变量传递是命脉,漏传一个变量,整条链路断裂
呼入转外呼(如 IVR 后转坐席)、外呼回呼(如短信通知后用户回拨)等场景,本质是channel variables 在不同 context 间的安全传递。FreeSWITCH 默认不透传变量,必须显式声明。
4.1 呼入转外呼:从 public context 到 outbound context 的变量桥接
典型场景:呼入 IVR 播报后,用户按 1 转接外线坐席(如 400 电话)。此时需将呼入的caller_id_number作为外呼主叫:
<!-- conf/dialplan/public.xml --> <context name="public"> <extension name="ivr_transfer"> <condition field="destination_number" expression="^9999$"> <!-- IVR 入口号 --> <action application="ivr" data="main_ivr"/> </condition> </extension> <!-- IVR 转外呼分支 --> <extension name="transfer_to_outbound"> <condition field="${ivr_result}" expression="^1$"> <!-- 用户按 1 --> <action application="set" data="outbound_target=4008001234"/> <action application="set" data="origination_caller_id_number=${caller_id_number}"/> <action application="set" data="origination_caller_id_name=${caller_id_name}"/> <action application="transfer" data="outbound_call XML public"/> </condition> </extension> </context> <!-- conf/dialplan/outbound.xml --> <context name="outbound"> <extension name="make_outbound"> <condition field="destination_number" expression="^400\d{3}\d{4}$"> <action application="log" data="INFO Outbound to ${destination_number} from ${origination_caller_id_number}"/> <action application="bridge" data="sofia/gateway/myisp/${destination_number}"/> </condition> </extension> </context>变量传递逻辑:
transfer应用将当前 channel 切换到outboundcontext,同时继承所有已设置的 variables;origination_caller_id_number和origination_caller_id_name是 FreeSWITCH 内置变量,bridge会自动将其映射为 SIPFrom头;- 若未显式
set,则外呼主叫为空,中继商拒接;transfer后必须跟XML public,表示 dialplan 文件名和 context 名,缺一不可。
4.2 外呼回呼:用 uuid 生成唯一回拨键,避免并发冲突
外呼通知后,用户回拨需关联原始外呼 session。不能用caller_id_number(多个用户同号),必须用 FreeSWITCH 分配的uuid:
<!-- 外呼发起时记录 uuid --> <action application="set" data="callback_uuid=${uuid}"/> <action application="set" data="callback_number=${caller_id_number}"/> <action application="log" data="INFO Callback UUID: ${callback_uuid} for ${callback_number}"/> <!-- 将 callback_uuid 编码为 DTMF 可播号码(如 1234567890ABCD) --> <action application="set" data="dtmf_code=${uuid:0:4}${uuid:24:4}${uuid:8:4}"/> <action application="playback" data="ivr/please_dial.wav"/> <action application="playback" data="ivr/callback_code.wav"/> <action application="say" data="${dtmf_code}"/>用户回拨时,IVR 采集 DTMF,再通过hash或数据库反查uuid,最后用uuid_broadcast触发事件唤醒原始 session:
<!-- 回拨匹配逻辑 --> <condition field="${dtmf_input}" expression="^([0-9A-F]{12})$"> <action application="set" data="target_uuid=${odbc(select uuid from callbacks where dtmf_code='${dtmf_input}' limit 1)}"/> <action application="uuid_broadcast" data="${target_uuid} playback::voicemail/vm-thankyou.wav"/> </condition>关键设计:
uuid全局唯一,128 位,截取 12 位足够区分(uuid:0:4取前 4 位,uuid:24:4取第 24 位起 4 位,uuid:8:4取第 8 位起 4 位);odbc查询必须用limit 1,防止多条记录导致uuid_broadcast广播到错误 session;uuid_broadcast是 FreeSWITCH 内置应用,无需额外模块,直接向指定 uuid channel 发送事件。
5. 避坑指南:FreeSWITCH 呼入呼出路由的五个血泪现场,现象、原因、解法全列清
FreeSWITCH 的坑不在代码,在配置的隐式依赖。以下五条是某公司线上系统连续三次凌晨告警后总结的硬核避坑清单,每一条都对应真实故障时间点与恢复操作。
5.1 现象:呼入电话响三声后自动挂断,fs_cli 中sofia status profile external显示UP,但无任何 error 日志
原因:external.xml中<param name="inbound-codec-prefs" value="..."/>未包含中继网关实际发送的编解码(如对方发 G.729,你只配了 PCMU/PCMA)。FreeSWITCH 收到 SDP 后发现无可协商 codec,静默 hangup。
解决:
- 开启
sofia global siptrace on,抓取呼入 INVITE 的 SDP; - 查看
m=audio行的a=rtpmap,确认对方支持的 codec(如18 G729/8000); - 将
G729加入inbound-codec-prefs,并确保 FreeSWITCH 已加载mod_g729(需商业授权); reloadxml后测试。
5.2 现象:外呼始终返回503 Service Unavailable,sofia status gateway myisp显示REGED,但INVITE发不出
原因:myisp.xml中<param name="proxy" value="..."/>填的是域名,但服务器/etc/hosts或 DNS 未解析该域名,或resolv.conf配置错误导致域名解析超时(默认 5 秒),Sofia 栈放弃重试。
解决:
- 在 FreeSWITCH 服务器执行
nslookup sip.myisp.com,确认解析正常; - 若解析慢,改用 IP 地址:
<param name="proxy" value="203.0.113.45:5060"/>; - 或在
vars.xml中增加<X-PRE-PROCESS cmd="set" data="dns_timeout=2"/>缩短 DNS 超时; sofia restart重载 profile。
5.3 现象:呼入号码匹配 dialplan 正则,但bridge user/1001@${domain_name}失败,日志报Can't find user 1001
原因:user/1001@${domain_name}中的domain_name变量为空,因为external.xml未配置<param name="domain" value="yourdomain.com"/>,FreeSWITCH 默认 domain 为localhost,而directory中用户存在1001@yourdomain.com。
解决:
- 在
external.xml<settings>中添加<param name="domain" value="yourdomain.com"/>; - 确保
conf/directory/default/1001.xml中<domain name="yourdomain.com">与之匹配; reloadxml后执行sofia status profile external,确认Domain:字段为yourdomain.com。
5.4 现象:外呼成功接通,但通话中一方听不到声音(单通),sofia status显示 RTP 流量为 0
原因:external.xml中<param name="rtp-ip" value="..."/>填的是内网 IP(如192.168.1.100),但中继商位于公网,尝试向该内网 IP 发 RTP 包,自然失败。
解决:
rtp-ip必须填服务器对外可见的公网 IP(如云服务器弹性 IP);- 若服务器在 NAT 后(如家庭宽带),必须配置
ext-rtp-ip为公网 IP,并在路由器做 16384-20000 端口映射; - 执行
tcpdump -i any portrange 16384-20000 -w rtp.pcap抓包,确认 RTP 包是否进出。
5.5 现象:transfer到另一个 context 后,origination_caller_id_number变为空,外呼主叫丢失
原因:transfer应用默认不继承所有 variables,仅继承部分内置变量;origination_caller_id_number需显式export才能透传。
解决:
- 在 transfer 前添加:
<action application="export" data="nolocal:origination_caller_id_number=${origination_caller_id_number}"/>; export的nolocal:前缀表示该变量将随 channel 一起迁移;- 在目标 context 中,用
${origination_caller_id_number}即可取到值; - 验证:在目标 extension 中加
<action application="log" data="DEBUG CID: ${origination_caller_id_number}"/>。
6. 进阶技巧:用 Lua 脚本动态路由,把硬编码 dialplan 变成可热更新的策略引擎
FreeSWITCH 的 XML dialplan 适合静态规则,但面对“工作日 9-18 点走 A 中继,其余时间走 B 中继”“VIP 用户优先用高码率编解码”等动态策略,XML 就力不从心。此时,Lua 脚本是最佳解法——它在 FreeSWITCH 进程内运行,毫秒级响应,且无需重启服务。
6.1 准备工作:启用 mod_lua 并验证脚本权限
首先确认conf/autoload_configs/modules.conf中已启用:
<load module="mod_lua"/>然后reloadxml,在fs_cli中执行:
luarun print("Hello from Lua!")若返回Hello from Lua!,说明 mod_lua 加载成功。
6.2 动态外呼路由脚本:根据时间、号码、用户等级选择 gateway
创建scripts/route_outbound.lua:
-- scripts/route_outbound.lua local function get_gateway_by_time() local hour = os.date("*t").hour if hour >= 9 and hour < 18 then return "myisp_premium" else return "myisp_basic" end end local function get_gateway_by_number(number) if string.match(number, "^1[3-9]%d{9}$") then return "myisp_mobile" elseif string.match(number, "^0%d{2,3}%d{7,8}$") then return "myisp_landline" else return "myisp_default" end end -- 主路由函数 function route_outbound(session, args) local number = session:getVariable("destination_number") local user_level = session:getVariable("user_level") or "standard" local gateway = "myisp_default" -- VIP 用户强制走 premium if user_level == "vip" then gateway = "myisp_premium" else -- 按时间路由 gateway = get_gateway_by_time() -- 按号码类型微调 if string.len(number) == 11 then gateway = get_gateway_by_number(number) end end -- 设置外呼参数 session:setVariable("origination_caller_id_number", session:getVariable("caller_id_number")) session:setVariable("effective_caller_id_number", session:getVariable("caller_id_number")) -- 执行 bridge session:execute("bridge", "sofia/gateway/" .. gateway .. "/" .. number) end6.3 在 dialplan 中调用 Lua 脚本
<!-- conf/dialplan/public.xml --> <context name="public"> <extension name="dynamic_outbound"> <condition field="destination_number" expression="^[\+0-9]+$"> <action application="lua" data="route_outbound.lua route_outbound"/> </condition> </extension> </context>执行逻辑:
lua应用加载route_outbound.lua,并调用其中的route_outbound函数;session对象可读取/设置任意 channel variable,如caller_id_number、user_level;session:execute("bridge", ...)等效于 dialplan 中的<action application="bridge" .../>;- 脚本修改变量后,后续 dialplan 步骤仍可使用(如
log记录选中的 gateway)。
6.4 热更新技巧:用 inotifywait 监控脚本变更,自动 reload
Lua 脚本修改后无需重启 FreeSWITCH,但需通知其重新加载。用 Linuxinotifywait实现:
# 安装 inotify-tools apt-get install inotify-tools # 创建监控脚本 reload_lua.sh #!/bin/bash while inotifywait -e modify /usr/local/freeswitch/scripts/route_outbound.lua; do echo "$(date): Reloading Lua script" echo "luarun package.reload('route_outbound')" | nc localhost 8021 done原理:FreeSWITCH 的 Event Socket(端口 8021)支持
luarun命令,package.reload强制重载 Lua 模块,实现秒级热更新。
从那以后我每次上线新路由策略,都先跑一遍inotifywait监控,再改脚本——再也不用reloadxml后祈祷所有 channel 没断。希望帮到你。
本文还有配套的精品资源,点击获取