前阵子帮一个客户做移动端呼叫功能,业务要求很简单:在微信内置浏览器和自家App里,用户点一下按钮就能发起语音通话,不需要安装任何SIP软电话。我第一反应是走WebRTC,把浏览器作为终端接入Freeswitch。但真上手之后才发现,信令通道怎么选、TLS证书怎么配、外网IP怎么绑定、ACL怎么放行,这几个问题一环扣一环,任何一个地方卡住,呼叫就是起不来。折腾了几天,我把wss接入、API调用简化、以及ACL权限控制整条链路都捋顺了,这里把我踩过的坑和配置思路完整记录下来,给后面要做同类需求的朋友一个参考。
需要说明的是,这篇文章面向的是已经装好Freeswitch、能跑基本呼叫的读者。如果你还在入门阶段,建议先把SIP注册和拨打流程玩熟,再回来看wss和ACL这些进阶配置,会顺很多。
1. 为什么要用wss:浏览器终端绕不开的一道协议坎
1.1 浏览器不支持UDP SIP,但支持WebSocket
浏览器里跑WebRTC,音视频走的是RTP/RTCP,信令走什么?最常见的做法是WebSocket。浏览器原生支持ws://和wss://,但不支持UDP包的SIP协议,所以想让浏览器和Freeswitch直接对接,就得有一个“翻译层”,把浏览器的WebSocket信令翻译成Freeswitch能懂的SIP或内部指令。
Freeswitch从比较早的版本就内置了mod_sofia的TLS支持,可以用wss://来承载SIP over WebSocket。这是最正统的WebRTC信令接入方案之一。浏览器发wss请求,Freeswitch监听在对应的TLS端口上,完成SIP注册、呼叫、挂断等操作,媒体面走WebRTC的SRTP,整体链路非常干净。
1.2 wss相比裸ws,解决的不只是加密问题
如果你的服务只在局域网里测试,用ws://确实省事,但到了生产环境,几乎必须上wss://。原因不复杂:很多公司网络和浏览器策略会拦掉不加密的WebSocket,而在HTTPS页面上调用ws://接口会被直接判定为混合内容,浏览器默认拒绝。只有wss://才能同时满足加密、合规、跨网络访问这几个要求。
所以在设计初期就把wss作为目标,别先上ws“跑通再说”。改动配置虽然不难,但后面加密证书、域名校验、ACL网段全部要跟着动,返工成本一点不小。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 配置前的准备工作:模块、证书、目录结构一次看清
2.1 模块加载顺序与依赖关系
Freeswitch启动时按modules.conf.xml里的顺序加载模块。做wss接入,核心依赖的是mod_sofia,它负责SIP栈和TLS监听。如果你还需要浏览器注册后通过mod_verto玩更高级的指令交互,那mod_verto也要加载,但最简方案只需要mod_sofia就够了。
我建议先检查模块有没有加载:
bash复制freeswitch -x "module_exists mod_sofia"
返回true就说明已经加载。如果没加载,去modules.conf.xml把这行取消注释:
xml复制<load module="mod_sofia"/>
<load module="mod_verto"/>
mod_verto走的是Verto协议,它天然支持WebSocket,并且自带心跳、订阅、指令路由机制。如果你的业务需要“浏览器直接发命令给Freeswitch”,比如控制分机状态、查询呼叫信息,那么mod_verto会省掉你一大半开发量。这个下面第4节展开讲。
2.2 TLS证书的三种准备方式
wss必须有TLS证书,这是最容易被卡住的一步。我在项目里试过三种方式,按实际场景给结论:
- 自签名证书:测试环境用,浏览器访问会告警,但在内网调通绰绰有余。生成后把证书放到Freeswitch的TLS目录就行。
- 内网CA签发:适合公司内部App,可以提前把CA根证书装进设备。用户体验比自签名好,但维护成本中等。
- 公网域名证书:生产环境首选,直接给服务器域名申请一张证书,浏览器零告警。
对于Freeswitch,证书格式没有太多讲究,PEM就行。关键是把证书私钥放在同一个目录里,文件名和配置里的路径要严格一致。我的做法是在/etc/freeswitch/tls/下放这几个文件:
code复制tls.crt
tls.key
fullchain.pem
fullchain.pem包含证书链,部分客户端(尤其是浏览器)校验不完整链会报错,所以强烈建议把它生成好。生成自签名证书的参考命令:
bash复制openssl req -x509 -newkey rsa:2048 -keyout tls.key -out tls.crt -days 365 -nodes -subj "/CN=fsserver.local"
cat tls.crt tls.key > fullchain.pem
chown -R freeswitch:freeswitch /etc/freeswitch/tls/
chmod 600 /etc/freeswitch/tls/tls.key
我遇到过证书文件权限不对,Freeswitch启动后TLS监听端口死活起不来的情况,所以权限一定要给够,私钥别设成644,否则日志里会报权限过于开放的警告。
2.3 目录结构要注意的细节
Freeswitch对配置目录的变动很敏感,建议修改前先备份。我通常把整个/etc/freeswitch/做个tar包。你后面要动vars.xml、sip_profiles、acl.conf.xml,这三个文件是wss配置的主战场,备份一次能省很多事。
3. 核心参数拆解:external SIP Profile的wss监听配置
3.1 先理解Freeswitch的Profile机制
SIP Profile是Freeswitch对“一个SIP监听实体”的定义,包括监听地址、端口、认证方案、媒体参数等。系统默认有internal(内网SIP,通常是5060端口)和external(外网SIP,通常是5080端口)两个Profile。做wss接入,可以直接在external上加上TLS监听参数,也可以单独建一个external-ssl.xml,两个互不影响。
根本需求是:你希望wss连接和传统SIP各走各的Profile,还是在同一个上监听?我建议单独建一个external-ssl.xml,因为TLS配置、ACL策略、拨号计划上下文通常都和普通SIP不一样,分开之后方便独立调整,遇到问题排查起来也干净。
3.2 一个可以照抄的external-ssl.xml
在/etc/freeswitch/sip_profiles/下新建external-ssl.xml,参考配置如下:
xml复制<profile name="external-ssl">
<gateways/>
<domains>
<domain name="all" alias="true" parse="true"/>
</domains>
<settings>
<param name="sip-port" value="7443"/>
<param name="context" value="public"/>
<param name="dialplan" value="XML"/>
<param name="tls" value="true"/>
<param name="wss" value="true"/>
<param name="tls-only" value="true"/>
<param name="tls-bind-ip" value="0.0.0.0"/>
<param name="tls-bind-port" value="7443"/>
<param name="wss-binding" value=":7443"/>
<param name="tls-cert-dir" value="/etc/freeswitch/tls"/>
<param name="apply-inbound-acl" value="ws_clients"/>
<param name="auth-calls" value="true"/>
<param name="apply-call-permission" value="true"/>
</settings>
</profile>
这里有个容易踩坑的点:wss-binding的端口必须和tls-bind-port一致。不少人在tls-bind-port写7443,wss-binding却写了别的端口,结果wss一直连不上。我的解决方案是干脆统一写成7443,避免混淆。
tls-only参数设成true后,该Profile不再接受普通SIP UDP/TCP信令,只接受TLS/WSS。如果你的服务器只有一个外网IP,把7443留给wss,把5080留给普通SIP,两者不冲突,能同时跑。
3.3 端口和内外网IP绑定的选择逻辑
关于监听地址,tls-bind-ip可以设成0.0.0.0,让Freeswitch在所有网卡上监听。但如果你需要限制只允许特定网段访问,更规范的做法是在ACL里做控制,而不是在监听层面写死IP。
外网部署时NAT环境最让人头疼。Freeswitch在vars.xml里有external_rtp_ip和external_sip_ip两个全局变量,用于告诉对端“你的媒体和信令该发到哪个公网地址”。很多呼叫建立失败、单通、无声,都与这两个变量没设对有关。我的经验:
xml复制<X-PRE-PROCESS cmd="set" data="external_sip_ip=你的公网IP或域名"/>
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=你的公网IP或域名"/>
如果你的服务器有动态公网IP,优先填域名,配合DNS解析,省得每次换IP都要改配置。这一点在wss场景下尤其重要,因为浏览器的RTC连接要能拿到正确的候选地址。
4. 简化API调用:从多端口管理到统一wss通道
4.1 为什么说“API简化”不是锦上添花
没有wss之前,你想用程序控制Freeswitch,通常要开Event Socket(8021端口)、HTTP API(8080端口)、XML接口等一堆端口,每种端口还要单独配认证、单独维护连接。终端多了之后,端口管理和鉴权就成了麻烦事。
wss接口天然可以承载双向消息,所以我的方案是:把Freeswitch对外暴露的控制能力收拢到一个wss入口上。浏览器或服务端只管连这个入口,不需要关心内部是走ESL还是走HTTP。
4.2 方案一:直接用mod_verto省掉自研网关
mod_verto是Freeswitch自带的WebSocket信令模块,它提供一个统一的wss接口,客户端连上来之后不但能注册、呼叫,还能通过JSON消息调用Freeswitch命令。它的好处在于:不用自己写WebSocket服务端,不用自己设计消息协议,Freeswitch已经把命令路由做好了。
浏览器侧的调用流程很简单:连上wss://你的域名:7443,发送一个带method字段的JSON请求,Freeswitch执行完命令后把结果推回来。举个简单例子:
json复制{
"verto": {
"method": "client_respond",
"sessionId": "会话ID",
"data": {
"event": "conference|command"
}
}
}
实际开发时我建议多翻一下Verto协议文档,它封装的指令比我想象中全。mod_verto存在的原因就是为了减少终端接入的定制工作,如果你的业务形态是浏览器为主,优先考虑。
4.3 方案二:自建轻量WSS网关,统一管理认证和命令
有些业务不想绑定Verto协议,或者服务端需要同时对接很多个Freeswitch实例,那就可以自建一个轻量WSS网关。大致架构是这样的:
- 客户端统一连自建网关(一个wss地址)
- 网关校验Token后,把收到的JSON指令转成ESL命令,发给Freeswitch的Event Socket
- Freeswitch处理完返回结果,网关再原路推回
这个方案的关键在于Event Socket连接的管理。我写过一版参考代码,核心就是建立一个ESL连接池:
js复制const esl = require('modesl');
const pool = [];
function getConn() {
if (pool.length) return pool.pop();
return esl.createClient({password: 'ClueCon', port: 8021, host: '127.0.0.1'});
}
function releaseConn(conn) {
if (pool.length < 10) pool.push(conn);
}
注意ClueCon是Freeswitch默认的ESL密码,生产环境一定要改。网关在转发指令时,只转发白名单内的方法名,以免恶意客户端拿到过大的控制权限,这也是“简化”之外必须搭配的约束。
4.4 简化之后的调用格式设计
不管用哪个方案,建议把对外API设计得足够简单。我最终定下来的格式是这样的:
json复制{
"action": "originate",
"params": {
"caller": "9999",
"callee": "1000",
"dialplan": "XML",
"context": "public"
}
}
网关和客户端只需要约定action和params两个字段,Freeswitch内部的复杂参数全部在网关层做映射。开发和联调阶段省下的时间非常可观,尤其是前端同学不需要了解SIP信令细节,只需关心业务字段。
5. ACL配置:wss接口的网段准入与权限边界
5.1 ACL在Freeswitch里的定位
ACL(Access Control List)是Freeswitch控制“谁能访问”的核心机制。它定义了一组IP地址或网段,并赋予放行或拒绝的规则。wss接口虽然加密了,但要控制哪里的用户能连、哪里的服务端能调用API,ACL是第一道防线。
配置文件在autoload_configs/acl.conf.xml,系统自带几个默认列表:lan、rfc1918.auto、loopback.auto、wan.auto。其中rfc1918.auto会自动匹配所有内网网段,wan.auto默认拒绝外网。我的建议是不要改默认列表,而是新建自己专属的列表,这样升级Freeswitch时不会因为默认列表变化而影响你已有规则。
5.2 新增一个ws_clients列表并绑定到Profile
在acl.conf.xml里加一个自定义列表:
xml复制<list name="ws_clients" default="deny">
<node type="allow" cidr="10.0.0.0/8"/>
<node type="allow" cidr="172.16.0.0/12"/>
<node type="allow" cidr="192.168.0.0/16"/>
<node type="allow" cidr="你的公网出口IP/32"/>
</list>
default="deny"表示除了明确allow的网段,其他一律拒绝,这是最安全的写法。如果你在公司内网调试,就把公司出口IP加进去;如果有固定用户群,就把用户所在网段加进去。
然后在external-ssl.xml里引用:
xml复制<param name="apply-inbound-acl" value="ws_clients"/>
这样配置之后,Freeswitch在wss握手阶段就会检查对方的IP,不在列表里的直接拒掉。这个策略对防止互联网上随便扫端口的扫描器非常有效。
5.3 用ACL同时控制API权限
ACL不仅能管SIP注册,还可以用来控制API调用来源。如果你走了自建WSS网关,网关的出口IP往往就是服务器的内网IP,那么你完全可以在acl.conf.xml里单列一个api_gateway列表,只允许网关IP访问ESL端口。这样就算ESL端口不小心暴露,外部也无法直接连上操作。
xml复制<list name="api_gateway" default="deny">
<node type="allow" cidr="127.0.0.1/32"/>
<node type="allow" cidr="10.0.1.5/32"/>
</list>
对应到Freeswitch的event_socket.conf.xml,把apply-inbound-acl指向它:
xml复制<param name="apply-inbound-acl" value="api_gateway"/>
5.4 动态IP环境下的ACL补充思路
如果客户端是移动网络,IP不固定,ACL写死网段就不现实了。这时候不要放弃ACL,而是把ACL权限缩小到“只允许通过网关转发”,客户端无论如何都连不到Freeswitch本体,管控焦点放在网关层,用Token或账号体系来鉴权。这个思路比“把ACL全部放开”安全得多。
6. 联调过程中最常出现的四个坑及排查思路
6.1 证书链校验失败
现象:浏览器wss连接报ERR_CERT_AUTHORITY_INVALID,WebSocket握手永远过不去。
排查思路:
- 先确认证书文件是否完整,
fullchain.pem是否存在且有效 - 用
openssl s_client -connect 你的域名:7443检查握手链路,看服务端返回的证书链是否完整 - 如果是自签名证书,测试浏览器也要把证书导入信任区
我遇到过最隐蔽的情况是证书文件末尾多了一个换行符,导致解析异常。所以建议用openssl x509 -in tls.crt -text -noout快速校验证书格式。
6.2 wss端口能通,但SIP注册一直超时
现象:端口扫描是开放的,浏览器WebSocket也能连上,但SIP OPTIONS和REGISTER总是超时。
排查思路:
- 确认
wss-binding和tls-bind-port一致 - 确认
tls-only设成了true,否则Freeswitch可能在同一个端口上混用TLS和非TLS逻辑,某些情况下会互相干扰 - 看日志:
tail -f /var/log/freeswitch/freeswitch.log,找[ERROR]和[WARNING]关键字
注册超时大多数不是ACL拦了,而是Profile参数不匹配。ACL拦掉通常直接表现为401或403响应,注册请求已经到达Freeswitch但被拒绝授权,这两类问题看日志就能区分。
6.3 ACL放行了但呼叫仍被拒
现象:注册没问题,但发起呼叫立刻失败,日志里有Reject或unauthorized之类的字眼。
这种情况大概率是auth-calls和apply-call-permission的联带效应。auth-calls=true要求SIP请求必须经过鉴权,如果你用的是wss内部指令而不是标准SIP注册流程,某些请求会被当成未认证请求拒掉。我的建议:如果终端都是内部自研客户端,auth-calls设成false,把认证责任交给应用层和ACL层;如果还有第三方SIP话机,保持true,但要在ACL里把话机网段也放行。
6.4 NAT环境下媒体流不通,能注册不能通话
现象:呼叫能建立,但听到对端的媒体流传不过去,或者单通。
排查思路:
- 确认
external_sip_ip和external_rtp_ip已经设成公网IP或域名 - 检查防火墙放行了RTP端口段(默认16384-32768),不要只放行SIP端口
- 用
ss -lntu检查UDP端口监听是否正常
这类问题和wss本身关系不大,但在WebRTC接入时特别容易出现,因为浏览器端的ICE协商要求Freeswitch必须在SDP里给出可达的公网候选地址。如果你的服务器有防火墙,一定要把RTP端口段和TCP/UDP规则一并处理。
下面整理一份常见的报错对照表,方便快速定位:
| 现象 | 可能原因 | 定位方式 |
|---|---|---|
| wss握手失败 | 证书不完整或端口不匹配 | openssl s_client检查、日志关键字TLS |
| 注册401/403 | ACL未放行或密码错误 | 看freeswitch.log、auth-calls状态 |
| 呼叫被拒 | auth-calls或apply-call-permission策略 |
临时关闭参数验证 |
| 能注册不能通话 | NAT内外网地址未设置 | 检查external_sip_ip、RTP端口段 |
7. wss长连接环境下的性能与维护要点
7.1 长连接数控制
wss不像SIP的UDP那样无状态,它是长连接。每个浏览器终端会持续占用一个TCP连接。如果用户量大,连接数会成为一个隐性瓶颈。建议在网关层做连接数统计,超过阈值时主动断掉空闲连接。Freeswitch侧的sip-timers配置也要关注,不要设置过长的注册有效期,否则用户掉线后仍要等很久才能重新注册。
7.2 日志与监控
wss接入的排错主要靠Freeswitch日志和浏览器控制台日志。Freeswitch侧:
bash复制freeswitch -x "console loglevel debug"
浏览器侧多看onerror回调,这个事件往往被前端同学忽略,其实大部分握手失败原因都会在它里面暴露。
7.3 证书到期管理
证书到期是wss接入最常见的“事故”。自签名证书到期前一周,浏览器就会开始出现握手警告。建议把证书过期时间写进监控系统,或者用脚本每天检查一次:
bash复制openssl x509 -enddate -noout -in /etc/freeswitch/tls/tls.crt
我之前就是因为忘了续期,导致线上客户端突然全部掉线,排查了半天才意识到是证书问题。这件事看起来很小,但在wss体系里,它的影响面比SIP还大,因为所有终端都是长连接,证书一过期,整体重连风暴会瞬间打到服务器上。
8. 最后再补充三点个人体会
这段不写“总结”,只分享三条我这几轮配置下来最想强调的经验。
第一条,ACL路由要留白,但也要设兜底。我在给某客户的多个分支网点配置时,一开始只加了两个网段,结果忙了一天发现还有一台服务器在第三个网段,连不上查了半天。后来我把ACL配置抽成了公共模板,新加网点只需改一行cidr。
第二条,API简化要跟着调用方走。我们一开始设计了很“完备”的消息协议,包含几十个字段,前端同学用起来非常痛苦,后来改成了前文那种只有action和params的极简格式,联调效率直接翻倍。你要记住,简化不是简单,而是降低心智负担。
第三条,wss方案里Freeswitch配置文件之间是有耦合的。改ACL会直接影响Profile的注册行为,改Profile又会影响vars.xml里的全局地址,最终环环相扣。建议每次改动之后,都用freeswitch -x "sofia status profile external-ssl"看一下Profile状态,确认监听正常,再继续往下调,不要一口气改完多个文件再集中排错,那样定位问题会很痛苦。
这套配置思路可以从单机测试扩展到多机负载。如果后面你在网关层加了多客户端的Token分组策略,或者把Freeswitch命令白名单做成了可配置的规则引擎,欢迎来交流实际效果,我也挺好奇不同规模下这套链路的表现差异。
