外观
5G 软电话 WebRTC ICE/TURN 部署、排障与验收
本文记录浏览器软电话在 5G 网络下从 ICE 协商失败、接通无声到恢复双向音频的完整处理经验, 可作为同类环境的部署基线、故障排查和上线验收手册。
文中的 192.168.2.198、123.57.205.60 和端口范围是当前环境的明确示例。复制到其他 环境前必须替换为现场地址,并同步校准 FRP、FreeSWITCH、防火墙、coturn 和应用配置。本文 不包含 shared secret、登录凭据或其他生产密钥。
1. 结论与当前基线
本次问题不是单独部署 TURN 就能解决,而是浏览器、coturn、FRP 和 FreeSWITCH 两侧都要形成 可达的 ICE 路径:
- 浏览器通过公网 coturn 获取
relaycandidate; - FreeSWITCH 只接受来自 coturn 公网 IP 的浏览器 candidate,避免选中 5G 运营商的
srflxcandidate; - FreeSWITCH 在 SDP 中广告经 FRP 映射的公网 RTP 地址,不能广告主机私网地址;
- coturn 控制流量和 relay 流量直接到公网机,不封装进 FRP;
- 发往 FreeSWITCH 公网 RTP 端口的流量才经过 FRPS → FRPC。
当前配置基线如下:
| 组件 | 地址或端口 | 用途 |
|---|---|---|
| FreeSWITCH | 192.168.2.198 | sip-ip / rtp-ip 的本机绑定地址 |
| FreeSWITCH 公网 SDP | 123.57.205.60 | ext-sip-ip / ext-rtp-ip 的广告地址 |
| FreeSWITCH RTP | 30000–30199/UDP | 经 FRPS / FRPC 映射到 FreeSWITCH |
| coturn | 123.57.205.60:3478/UDP | STUN 和 TURN/UDP |
| coturn | 123.57.205.60:3478/TCP | TURN/TCP 回退 |
| coturn relay | 123.57.205.60:31000–31040/UDP | 浏览器媒体 relay,不经过 FRP |
2. 实际拓扑和媒体路径
text
手机浏览器(5G)
├─ SIP WS/WSS 信令 ───────────────> FRPS 公网入口 ─> FRPC ─> FreeSWITCH
│
└─ STUN/TURN :3478/TCP+UDP ──────> coturn(公网机,直接访问,不经过 FRP)
│
└─ relay :31000–31040/UDP
↕ WebRTC 音频
FRPS :30000–30199/UDP
│
└─ FRPC ─> FreeSWITCH
192.168.2.198:
30000–30199/UDP需要区分两个公网 UDP 范围:
31000–31040是 coturn 自己分配的 relay 端口,coturn 必须直接监听并由公网防火墙放行;30000–30199是 FreeSWITCH RTP 公网映射,FRPS 接收后由 FRPC 转发到192.168.2.198:30000–30199;- 两个范围不能重叠,也不能把 TURN relay 范围错误地配置成 FRP 转发范围;
- SIP WS/WSS 走 FRP 只代表信令可达,不代表 WebRTC 媒体已经可达。
3. 故障演进与证据
3.1 阶段一:未部署 TURN,ICE 协商失败
手机切到 5G 后,浏览器通常只能提供 host 和运营商 NAT 形成的 srflx candidate。运营商 CGNAT、防火墙或不对称路由会使这些地址无法被 FreeSWITCH 稳定访问。
典型表现包括:
- 浏览器没有可用的
relaycandidate; - ICE gathering 虽能完成,但 candidate pair 无法进入可用状态;
- SIP 呼叫可能收到
488 Not Acceptable Here,或页面直接报告 ICE / 媒体连接失败; - 同一设备在局域网可用,切换到 5G 后稳定复现。
此阶段需要先部署公网 TURN,并确认 UDP、TCP 两种 allocation 都成功。只增加一个 STUN 地址 不能解决运营商 CGNAT 下的媒体可达性。
3.2 阶段二:部署 TURN 后接通,但双方无声
coturn 上线后,浏览器能够获得 relay candidate,SIP 也能接通,但媒体仍可能完全不通。 本次现场同时出现了以下证据:
- FreeSWITCH 收到
srflx和relaycandidate 后,日志仍显示Choose rtp candidate ... srflx; - FreeSWITCH 的 200 OK SDP 中,
c=或本地 candidate 仍广告192.168.2.198; - ICE 会话已建立或正在检查,但 DTLS 长时间停在
HANDSHAKE; - Opus 已协商,收发 RTP 帧或包计数仍为
0; - SIP 状态显示接通,浏览器和对端却都听不到声音。
“已接通”只证明 SIP 信令完成,不能证明 ICE、DTLS-SRTP 和 RTP 已成功。
4. 两个根因
4.1 FRP 回送信令被误判为本地连接
浏览器的 SIP WS/WSS 先到公网 FRPS,再由内网 FRPC 回送 FreeSWITCH。FreeSWITCH 看到的 network_ip 因此是本机内网来源,会命中 localnet.auto。Sofia 将呼叫当成本地连接处理时, ext-rtp-ip 不会进入 SDP,最终把不可从公网访问的 192.168.2.198 发给浏览器。
当前 WebRTC 所在的 internal profile 使用:
xml
<param name="local-network-acl" value="none"/>这会强制该 profile 走 NAT 分支,使 ext-rtp-ip=123.57.205.60 进入对外 SDP。该设置应按 profile 的实际用途评估;如果同一 profile 还服务复杂的纯内网终端,需要在变更前回归这些终端。
4.2 FreeSWITCH 1.10 不会自动回退到 TURN relay
FreeSWITCH 1.10 默认会优先选择允许的非 relay candidate。本次它选择了手机的运营商 srflx,即使同一份 SDP 中已经有 coturn relay,也不会在该路径不通时自动切换到 relay。
解决方式不是继续增加 TURN 地址,而是通过 apply-candidate-acl 只允许 coturn 公网 IP:
xml
<list name="softphone-turn-relay" default="deny">
<node type="allow" cidr="123.57.205.60/32"/>
</list>xml
<param name="apply-candidate-acl" value="softphone-turn-relay"/>普通 SIP 终端不携带 WebRTC ICE candidate,因此不会被这条 candidate ACL 直接过滤。若更换 coturn 公网 IP,必须先更新 ACL,再切换 DNS 或应用下发地址。
5. 最终配置基线
5.1 coturn
Debian 12 公网主机的安装、配置权限、防火墙、systemd 启停、鉴权 allocation 验收和回滚步骤见 coturn/README.md。本节只保留跨组件配置基线和原理说明,仓库参考文件为 coturn/turnserver.conf.example:
ini
listening-ip=0.0.0.0
listening-port=3478
external-ip=123.57.205.60/<coturn-private-ip>
min-port=31000
max-port=31040
realm=softphone.ai-voice-platform
use-auth-secret
static-auth-secret=<server-private-shared-secret>
fingerprint
no-tls
no-dtls部署要求:
- 公网安全组和主机防火墙放行
3478/TCP+UDP、31000–31040/UDP; external-ip左侧为公网 IP,右侧为 coturn 主机实际绑定的私网 IP;- 使用 coturn REST shared-secret 鉴权,不创建长期浏览器账号;
- shared secret 只进入 coturn 私有配置、后端环境变量或密钥系统,不写入前端和仓库;
- 当前未启用 TLS/TURNS,浏览器通过
turn:...?...transport=tcp做 TCP 回退。
5.2 FreeSWITCH 和 FRP
freeswitch/vars.xml 中将绑定地址与公网广告地址分离:
xml
<X-PRE-PROCESS cmd="set" data="domain=192.168.2.198"/>
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=123.57.205.60"/>
<X-PRE-PROCESS cmd="set" data="external_sip_ip=123.57.205.60"/>freeswitch/sip_profiles/internal.xml 使用本机地址绑定, 使用公网地址生成 SDP,并应用 NAT 与 candidate 策略:
xml
<param name="rtp-ip" value="$${local_ip_v4}"/>
<param name="sip-ip" value="$${local_ip_v4}"/>
<param name="local-network-acl" value="none"/>
<param name="apply-candidate-acl" value="softphone-turn-relay"/>
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>freeswitch/autoload_configs/acl.conf.xml 中 softphone-turn-relay 只允许 coturn 公网 IP; freeswitch/autoload_configs/switch.conf.xml 中 RTP 范围为:
xml
<param name="rtp-start-port" value="30000"/>
<param name="rtp-end-port" value="30199"/>FRPS / FRPC 需要逐端口或按所用 FRP 版本支持的范围映射完整转发 30000–30199/UDP。公网安全组、FRPS 监听、FRPC 转发和 FreeSWITCH 主机防火墙四处必须一致。
5.3 应用后端与前端
callflow-server 和 callout-server 均通过受登录鉴权保护的 GET /api/softphone/ice-config 下发 ICE 配置。仓库和部署样例默认关闭 TURN:
json
{
"turn": {
"enabled": false,
"host": "123.57.205.60",
"port": 3478,
"realm": "softphone.ai-voice-platform",
"sharedSecret": "",
"credentialTtlSeconds": 3600
}
}本地直连时接口返回:
json
{ "enabled": false, "iceServers": [], "expiresAt": null }关闭状态不校验 host、realm 或 shared secret,不生成凭据。前端不会缓存关闭状态或安排刷新, 后续注册、拨号和接听会重新请求配置。
生产环境应通过私有配置把 enabled 改为 true,并通过 CALLFLOW_SERVER_TURN_SHARED_SECRET、CALLOUT_TURN_SHARED_SECRET 或服务器私有配置注入 真实 shared secret。后端按 coturn REST 规范生成 <过期 Unix 时间>:softphone 用户名以及 HMAC-SHA1/Base64 credential,接口返回:
json
{
"enabled": true,
"iceServers": [
{ "urls": "stun:turn.example.com:3478" },
{
"urls": [
"turn:turn.example.com:3478?transport=udp",
"turn:turn.example.com:3478?transport=tcp"
],
"username": "<expiry>:softphone",
"credential": "<HMAC-SHA1/Base64>"
}
],
"expiresAt": "<ISO time>"
}启用但缺少 host、realm 或 shared secret 时返回 503 ICE_CONFIG_UNAVAILABLE。所有成功和 配置错误响应均带 Cache-Control: no-store。
前端对短期凭据执行以下策略:
- 在到期前 5 分钟主动刷新;
- 刷新失败时每 30 秒重试,尚未过期的缓存可以继续用于新 SDP;
- 缓存已过期且刷新仍失败时,阻断新的软电话注册、呼叫或接听,不用过期凭据继续尝试;
- 刷新不替换已经建立的 JsSIP UA 或正在通话的 PeerConnection,新凭据供后续 SDP 使用。
6. 标准变更步骤
FreeSWITCH profile 重启会中断注册并可能影响通话,应在维护窗口执行。
确认当前没有活动呼叫,并记录现有注册数:
bashfs_cli -x "show calls count" fs_cli -x "sofia status profile internal reg"备份将修改的 XML。路径按现场
conf_dir调整:bashFS_CONF=/etc/freeswitch STAMP=$(date +%Y%m%d-%H%M%S) sudo cp -a "$FS_CONF/vars.xml" "$FS_CONF/vars.xml.bak.$STAMP" sudo cp -a "$FS_CONF/autoload_configs/acl.conf.xml" \ "$FS_CONF/autoload_configs/acl.conf.xml.bak.$STAMP" sudo cp -a "$FS_CONF/autoload_configs/switch.conf.xml" \ "$FS_CONF/autoload_configs/switch.conf.xml.bak.$STAMP" sudo cp -a "$FS_CONF/sip_profiles/internal.xml" \ "$FS_CONF/sip_profiles/internal.xml.bak.$STAMP"合并现场配置并做 XML 语法检查:
bashxmllint --noout "$FS_CONF/vars.xml" \ "$FS_CONF/autoload_configs/acl.conf.xml" \ "$FS_CONF/autoload_configs/switch.conf.xml" \ "$FS_CONF/sip_profiles/internal.xml"依次加载 XML、刷新 ACL、验证 ACL 命中,再重启 Sofia profile:
bashfs_cli -x "reloadxml" fs_cli -x "reloadacl" fs_cli -x "acl 123.57.205.60 softphone-turn-relay" fs_cli -x "sofia profile internal restart reloadxml"等待软电话重新注册。注册恢复后只建立一通新测试呼叫,不用变更前已存在的通话判断结果:
bashfs_cli -x "sofia status profile internal" fs_cli -x "sofia status profile internal reg"
任何命令返回错误、ACL 未命中或注册未恢复时,应停止扩大测试范围并按第 8 节回滚。
7. 验收清单与关键命令
7.1 短期凭据接口
以下命令中的地址和 token 都是占位变量。不要把真实登录凭据或 shared secret 写入脚本、工单或 命令行历史。
未登录请求必须返回 HTTP
401:bashBASE_URL='https://management.example.com' curl -sS -o /dev/null -w '%{http_code}\n' \ "$BASE_URL/api/softphone/ice-config"使用已登录会话的 Bearer token 请求,响应必须是
200且禁止缓存:bashACCESS_TOKEN='REPLACE_WITH_LOGIN_BEARER_TOKEN' curl -sS -D /tmp/softphone-ice.headers \ -H "Authorization: Bearer $ACCESS_TOKEN" \ "$BASE_URL/api/softphone/ice-config" \ -o /tmp/softphone-ice.json grep -i '^cache-control: *no-store' /tmp/softphone-ice.headers jq -e '.code == "0000" and (.data.enabled | type == "boolean")' \ /tmp/softphone-ice.json本地关闭模式应返回空列表和空过期时间:
bashjq -e ' .data.enabled == false and .data.iceServers == [] and .data.expiresAt == null ' /tmp/softphone-ice.json生产启用模式应检查响应同时包含 STUN、TURN/UDP、TURN/TCP,用户名时间戳和
expiresAt与配置 TTL 一致。当前 TTL 是3600秒,应允许少量命令执行耗时:bashjq -e '.data.enabled == true and (.data.expiresAt | fromdateiso8601 > now)' \ /tmp/softphone-ice.json jq '.data | {enabled, iceServers, expiresAt}' /tmp/softphone-ice.json jq -e ' [.data.iceServers[].urls | if type == "array" then .[] else . end] as $urls | ($urls | map(startswith("stun:")) | any) and ($urls | map(contains("transport=udp")) | any) and ($urls | map(contains("transport=tcp")) | any) ' /tmp/softphone-ice.json TURN_USERNAME=$(jq -r \ '.data.iceServers[] | select(.username != null) | .username' \ /tmp/softphone-ice.json) NOW=$(date +%s) EXPIRES=${TURN_USERNAME%%:*} test "$EXPIRES" -gt "$NOW" test $((EXPIRES - NOW)) -le 3600 test "$(date -d "$(jq -r '.data.expiresAt' /tmp/softphone-ice.json)" +%s)" \ -eq "$EXPIRES"HMAC 校验只能在能够安全读取 shared secret 的后端或受控运维机执行:
bashTURN_CREDENTIAL=$(jq -r \ '.data.iceServers[] | select(.credential != null) | .credential' \ /tmp/softphone-ice.json) EXPECTED=$(printf '%s' "$TURN_USERNAME" | \ openssl dgst -sha1 -hmac "$TURN_SHARED_SECRET" -binary | openssl base64 -A) test "$TURN_CREDENTIAL" = "$EXPECTED"
验收后删除临时响应文件,并清除包含短期 token 的临时环境:
bash
rm -f /tmp/softphone-ice.headers /tmp/softphone-ice.json
unset ACCESS_TOKEN TURN_USERNAME TURN_CREDENTIAL EXPECTED TURN_SHARED_SECRET浏览器端永远不应拿到 shared secret,只能拿到短期 username 和 credential。
7.2 TURN allocation
在独立外网或 5G 网络上使用 coturn 自带工具测试,不能只在 coturn 本机回环测试:
bash
TURN_HOST=123.57.205.60
turnutils_uclient -v -y -u "$TURN_USERNAME" -w "$TURN_CREDENTIAL" \
-p 3478 "$TURN_HOST"
turnutils_uclient -v -y -t -u "$TURN_USERNAME" -w "$TURN_CREDENTIAL" \
-p 3478 "$TURN_HOST"第一条验证 UDP 到 TURN,第二条验证 TCP 到 TURN。两者都应完成 authenticated allocation; 日志或抓包中的 relayed address 必须位于 123.57.205.60:31000–31040。若工具版本的参数有 差异,以目标机 turnutils_uclient --help 为准,但必须分别覆盖 UDP 和 TCP。
同时在 coturn 主机确认监听和 allocation 日志:
bash
sudo ss -lntup | grep ':3478'
sudo ss -lunp | grep 'turnserver'
sudo journalctl -u coturn --since '10 minutes ago'7.3 新通话的 ICE、DTLS 和 RTP
建立一通新的 5G 软电话测试呼叫,逐项确认:
- 浏览器的选中 candidate pair 中,本地候选类型是
relay,地址是123.57.205.60:310xx;可在chrome://webrtc-internals或等价 WebRTC 诊断页查看; - FreeSWITCH 日志的
Choose rtp candidate选择同一个123.57.205.60:310xx,不能再选 手机运营商srflx; - FreeSWITCH 200 OK SDP 的
c=和媒体 candidate 广告123.57.205.60:300xx,不能出现192.168.2.198; - ICE 进入成功状态,DTLS 完成握手而不是长期停在
HANDSHAKE; - Opus 入站和出站 RTP 包计数持续增长;
- 手机与对端分别朗读不同测试短句,确认双向音频,而不是只验证单向播放。
可用以下命令辅助检查,CALL_UUID 替换为本次新呼叫 UUID:
bash
CALL_UUID='REPLACE_WITH_NEW_CALL_UUID'
fs_cli -x "uuid_getvar $CALL_UUID remote_media_ip"
fs_cli -x "uuid_getvar $CALL_UUID remote_media_port"
fs_cli -x "uuid_getvar $CALL_UUID switch_r_sdp"
fs_cli -x "uuid_dump $CALL_UUID" | \
grep -Ei 'candidate|ice|dtls|rtp_audio_(in|out)_packet_count|media_(ip|port)'
sudo grep -E 'Choose rtp candidate|DTLS|HANDSHAKE' \
/var/log/freeswitch/freeswitch.log | tail -n 100现场日志路径和变量可因 FreeSWITCH 构建或日志配置不同而变化。必要时短时启用 SIP trace 或 uuid_debug_media,采样完成立即关闭,避免长期产生大量日志或记录敏感 SIP 信息。
8. 回滚
如果变更后注册、内网 SIP 终端或媒体出现回归:
- 停止建立新呼叫并确认活动呼叫已结束;
- 将第 6 节同一时间戳的四份
.bak.<STAMP>文件恢复到原路径; - 执行
xmllint --noout,然后依次执行reloadxml、reloadacl和sofia profile internal restart reloadxml; - 等待软电话和其他终端重新注册,验证原有基线;
- 如果同时变更了 coturn 或应用 TURN 配置,也恢复对应私有配置并重启该组件,确保后端与 coturn 的 shared secret 始终成对一致。
不要只回滚 ACL 而保留 apply-candidate-acl 引用,否则新 WebRTC 呼叫可能没有任何可用 candidate。不要通过删除日志或覆盖现有 XML 来“回滚”。
9. 容量与安全注意事项
31000–31040只有 41 个 relay 端口。每个 allocation 会消耗 relay 资源,实际并发上限还 受浏览器行为、端口保留、coturn 配额、CPU 和带宽影响;上线前必须按峰值并发压测并留余量;30000–30199也限制 FreeSWITCH 可用 RTP 会话规模。不要仅按端口个数直接等同通话数, 应结合 RTCP mux、会议、转接和并发腿数实测;- 经 TURN 的音频会增加公网机双向转发带宽和包处理量,应监控 allocation 数、丢包、抖动、 出入带宽、文件描述符和端口耗尽;
- TURN
3478/TCP是 UDP 受限网络的回退路径,不等于 TURNS。若部署 HTTPS 页面,应同时确保 SIP 信令符合浏览器的 WSS 和混合内容规则; getUserMedia()在常规浏览器中要求安全上下文,生产页面应使用 HTTPS;localhost是开发 特例。手机通过 5G 打开普通 HTTP 公网 IP 页面时,麦克风可能直接被浏览器拒绝,TURN 无法 绕过此限制;- TURN shared secret、应用登录 token 和短期 TURN credential 都不应进入日志、截图、工单 或代码仓库。短期 credential 虽会过期,仍应按凭据处理。
10. 常见误区
- 只配置
ext-rtp-ip。 FRP 回送源命中localnet.auto时,公网地址仍可能不进入 SDP; 必须同时确认 NAT 分支和实际 200 OK SDP。 - 只执行
reloadxml。 ACL 缓存需要reloadacl,Sofia profile 参数需要重启对应 profile 才能对新注册和新通话生效。 - 认为 TURN 会自动覆盖 candidate 选择。 TURN 只提供
relaycandidate;FreeSWITCH 1.10 仍可能优先选择srflx,需要apply-candidate-acl明确限制。 - 只看 SIP 200 OK。 SIP 接通不代表 ICE、DTLS 或 RTP 成功,必须检查 candidate pair、 DTLS 状态、RTP 计数和双向听感。
- 把 TURN relay 也放进 FRP。 当前拓扑中 coturn 直接位于公网机,只有 SIP WS/WSS 和 FreeSWITCH RTP 映射经过 FRP。
- 用旧通话验证新配置。 ICE candidate 和 SDP 在建连时确定;配置变更后必须等待重新注册并 建立新呼叫。
- 把公网 IP 填进
sip-ip/rtp-ip。 这两个参数必须绑定主机真实拥有的接口地址;公网 地址只用于ext-sip-ip/ext-rtp-ip广告。