本篇聚焦自建部署:STUN/TURN/ICE 到底各自解决什么问题、哪些环节真的需要 TURN、单机能撑多少路、带宽怎么算钱、coturn 生产配置与安全加固怎么写、443 端口怎么和 Web 服务共存、多区域高可用怎么做,以及自建与托管的成本临界点在哪。
| 名词 | 解决什么 | 流量特征 | 成本量级 | 现行 RFC |
|---|---|---|---|---|
| STUN | 我在 NAT 后的公网映射地址是什么(srflx 候选)+ ICE 连通性检查 | 只有信令级小包(几百字节 × 几次),媒体不经过它 | 近似为零 | RFC 8489(原 5389) |
| TURN | 打洞失败时由服务器中继全部媒体(relay 候选) | 全部媒体流经服务器,进出双向 | = 出网流量费,主成本 | RFC 8656(原 5766);IPv6 扩展 6156;TCP relay 6062 |
| TURNS | TURN over TLS/DTLS,穿透深度包检测防火墙(常挂 443) | 同 TURN,加 TLS 握手与加密开销 | 同 TURN | 同 8656 |
| ICE | 把 host / srflx / relay 候选排优先级并竞速选路 | — | — | RFC 8445(原 5245) |
| NAT / 网络类型 | 行为 | 直连可能性 |
|---|---|---|
| Full-cone / Restricted cone | 映射对外稳定 | STUN 打洞基本成功 |
| Port-restricted cone | 需先向对端确切 IP:端口发过包 | 看对端类型,多数可成 |
| Symmetric NAT(含多数 CGNAT) | 每个远端目标一个新映射,端口不可预测 | 双方都是对称型时必须中继 |
| 企业防火墙 / 酒店网络 | UDP 全禁,只放 TCP 443 | 必须 TURN over TLS 443 |
移动网络的运营商级 NAT(CGNAT)是打洞失败的主因,跨双层 CGNAT 时几乎必然回落中继(APNIC)。反向的好消息是 IPv6 双端可用即可绕过 NAT 直连:全球 IPv6 采用率已约 49.6%(Google,2026-07-26),越南约 65%+、墨西哥约 57.7%,而印尼仅约 26%——同样的产品,在印尼要按更高的中继占比做预算,在越南/墨西哥可以更乐观。
| 链路 | 是否需要 TURN | 原因 | 预算口径 |
|---|---|---|---|
| AI 外呼 / 呼入:FreeSWITCH ↔ 线路商 SBC(SIP/RTP) | 不需要 | 服务器对服务器,双方公网 IP 固定,不存在 NAT 穿透问题;被叫是普通电话,媒体走 PSTN | 0 |
| 浏览器网页座席 → 自家媒体网关/SFU(公网 IP,ICE-Lite) | 兜底需要 | 服务器侧公网可达,多数直连;只有企业防火墙封 UDP 时回落 TURN/TCP-443 | 按座席通话时长 × 10–20% 中继占比 |
| 移动 App 客户端 → 自家媒体服务器 | 兜底需要 | CGNAT + 封 UDP 的移动网络需中继 | 同上,CGNAT 重的市场取上限 |
| 真 P2P(网页回呼、双端都在 NAT 后的视频客服) | 必备 | 两端都可能是对称 NAT | 按 20% 中继率(行业均值)预算,企业网络场景可达 40–60% |
RTCPeerConnection 的选中候选类型(relay / srflx / host)并按国家、网络类型分维度统计——用自己的 relay 占比替换所有二手数字,这是唯一能决定容量与成本的输入。# 月度中继流量速算公式(出网口径)
月 GB = 单向码率(kbps) ÷ 8 ÷ 1024 × 3600 ÷ 1024 × 通话小时 × 中继占比 × 中继方向数
# 例 A:50 席网页座席,人均 6 通话小时/天 × 22 天 = 6,600 小时/月,音频 50kbps,中继占比 20%,双向
6600 × 0.2 × 0.045 GB/h ≈ 59 GB/月 → Cloudflare 免费额度(1 TB)内,$0
# 例 B:10 万分钟/月 PSTN 外呼(SIP 中继)
0 GB ← 不经 TURN
# 例 C:同样 6,600 小时但是 720p 视频客服,中继占比 20%,双向
6600 × 0.2 × 1.35 GB/h ≈ 1,782 GB/月 → 托管 Twilio 新加坡 ≈ $1,069/Cloudflare ≈ $39/自建 VPS ≈ $20–40
常见口径(CelloIP 生产指南):4 vCPU / 8 GB / 1 Gbps 网卡可撑 5,000–10,000 路并发音频中继或 500–1,000 路 720p 视频。用上面的双向公式核一下:
| 场景 | 厂商上限 | 反推所需网卡吞吐 | 1 Gbps 网卡实际可承载(留 40% 余量) |
|---|---|---|---|
| 音频 50 kbps 单向中继 | 10,000 路 | 10,000 × 50k × 2 = 1.0 Gbps(刚好打满) | 约 6,000 路 |
| 音频,双向都经同一 TURN | 10,000 路 | 2.0 Gbps(超) | 约 3,000 通 |
| 720p 视频 1.5 Mbps | 1,000 路 | 1,000 × 1.5M × 2 = 3.0 Gbps(远超) | 约 200–300 路 |
结论:厂商上限本质是"网卡刚好打满"的理论值,不是可运营容量。TURN 容量规划应当先算网卡,再谈 CPU——coturn 官方 Wiki 也把 NIC/RSS、UDP 哈希表、文件描述符列为主要瓶颈,CPU 反而不是第一约束。
min-port=49152 / max-port=65535 = 16,384 个端口,每个 allocation 至少占 1 个 → 单实例硬上限约 1.6 万 allocation。coturn 4.13.0 起的 --multiplex-peer 用"每线程共享中继 socket"取代"每 allocation 一个端口",官方称对不依赖唯一中继端口的系统能显著提升容量(官方未给倍数,需自测)。LimitNOFILE=1048576 + fs.file-max。rmem_max/wmem_max、netdev_max_backlog。coturn 4.13.0 已在 Linux 上默认开启 --udp-recvmmsg(批量收包),4.14.0 重写了 Prometheus 支持并暴露 recvmmsg/sendmmsg 批量指标。↔ 表格可横向滑动 | 单价为 2026-07-30 官方牌价口径
| 方案 | 单价 | 免费额度 | 区域/接入 | 要点 |
|---|---|---|---|---|
| Cloudflare Realtime TURN | $0.05/GB(仅出网计费) | 1,000 GB/月;与其 SFU 搭配时 TURN 免费;STUN 免费无限 | Anycast 全球就近,无区域差价 | 性价比与延迟双优;不支持 TCP relay(RFC 6062),凭据最长 48 小时,需支持 ICE restart |
| Twilio NTS | 美西/欧洲 $0.40;新加坡/孟买/东京 $0.60;悉尼/圣保罗 $0.80 | STUN 免费 | 五大洲固定 PoP | 恰好在我们的两个重点区域(东南亚、拉美)最贵;ICEPerf 实测其 TURN 候选延迟明显落后 |
| Metered.ca | 套餐制 $99/150 GB、$199/500 GB、$499/2 TB(折合 $0.25–0.66/GB) | 有免费档(口径不一) | 31+ 区域,含新加坡、圣保罗 | 区域覆盖好,单价中等 |
| Xirsys | $39/50 GB、$99/150 GB、$199/500 GB、$499/2 TB(超量 $0.09–0.50/GB) | 免费档 500 MB | PoP 以美欧为主 | 亚太/拉美覆盖弱 |
| 腾讯云 / 阿里云 | 无独立 TURN/STUN 计费产品 | — | — | 官方路径是整套 RTC(TRTC / 阿里云 RTC,按分钟计费);腾讯 TRTC 文档里 coturn 只作为"企业内网代理"出现——即需要独立 TURN 时官方也让你自建 |
| 自建 coturn(大云) | 出网 $0.08–0.12/GB(AWS/GCP/Azure/阿里国际)→ $80–120/TB | AWS 100 GB/月、GCP 200 GB/月 | 区域最全 | 大云出网单价比 Cloudflare TURN 贵一倍以上——在大云上自建 TURN 省不了钱 |
| 自建 coturn(含流量套餐 VPS) | Hetzner 含 20 TB/月、超量约 €1/TB;DO/Vultr 含 1–4 TB、超量 $0.01/GB | — | Hetzner 无拉美机房;Vultr 有新加坡/圣保罗 | $20–60/月即可覆盖 TB 级中继,自建省钱的前提是选对流量套餐主机 |
| 实现 | 定位 | 活跃度(2026-07) | 适用 | 短板 |
|---|---|---|---|---|
| coturn 推荐 | C 语言,功能最全的 STUN/TURN 守护进程;多数据库后端、OAuth、CLI/Web 管理面、Prometheus | 极活跃:源码 4.15.0(2026-07-22)、官方镜像 4.16.0-r0(2026-07-30);两个月连发 5 个版本;14.2k stars | VM/裸机、协议完备性要求高、要 TCP/TLS/DTLS 全支持 | 老 C 代码库安全面大(2026 年连续修 CVE);配置项极多易踩坑;官方明确不提供 Helm chart |
| eturnal | Erlang/OTP(ProcessOne,源自 ejabberd 模块),YAML 配置,内置 TURN REST 临时凭据 | 仓库有维护(近期还在升 OTP),但发布节奏约一年一版(1.12.2 / 2025-05) | XMPP 生态、想要低运维单体、高并发连接 | 社区规模远小;官网只声明支持 RFC 5389/5766/6156(未宣称 8656) |
| pion/turn v4 | Go 库,不是开箱服务:自己写壳、自定义认证与配额 | 活跃(pion 生态底座;STUNner 数据面即基于它) | Go 服务内嵌 TURN、认证要与业务系统深度耦合 | 无用户库/管理面/运维工具,全部自建 |
| STUNner(l7mp) | Kubernetes 的 WebRTC 媒体网关:用 Gateway API(Gateway/UDPRoute CRD)声明式管 TURN 入口 | 活跃,有官方 Helm chart 与 managed dataplane | 媒体服务器跑在 K8s 私有 Pod 网络、要弹性伸缩且不想用 hostNetwork | 与 K8s 强绑定;背后是小公司;协议特性不如 coturn 全 |
| restund | C,模块化插件(现挂 baresip 组织),历史最大用户是 Wire | 低 | Wire 存量生态 | 新项目不建议 |
| turn-rs / violet | Rust / 轻量 C++ 实现 | 小众 | 简单场景替代 coturn、嵌入式 | 生产案例少 |
--udp-recvmmsg Linux 默认开启、--multiplex-peer)、可观测(Prometheus 重写、支持 HTTPS 指标端点)、协议(RFC 8016 mobility 平滑切换)与大量安全修复。helm install 表达。turn.tls_port 必须是 443(这是对客户端广告的端口);挂 L4 LB 时 443→5349 并设 external_tls: true;云上还要 rtc.use_external_ip: true。stream + ssl_preread 按 SNI 分流。hostNetwork: true——这是官方文档明示的反模式(每节点只能一个媒体 Pod、公网 IP 上全 UDP 敞开),且 GKE Autopilot 直接禁用 hostNetwork。要云原生就上 STUNner,或干脆把 TURN 放回 VM。coturn/coturn(因 relay 端口范围大,容器官方建议 --network=host)。装完先 turnserver --version 确认 ≥ 4.15。.path 监听证书文件变化自动 reload coturn(续期后不 reload 会静默失效)。use-auth-secret 时限凭据(HMAC-SHA1,后端签发),绝不把静态用户名密码或 secret 下发到浏览器。user-quota / total-quota / max-bps / bps-capacity + fd 与 UDP buffer。# ── 监听与中继 ─────────────────────────────
listening-port=3478
tls-listening-port=5349
alt-tls-listening-port=443 # 企业防火墙"核选项";与 Web 共 IP 时见第七章
listening-ip=0.0.0.0
external-ip=<公网IP> # 云上 1:1 NAT 必填,可写 公网IP/内网IP
relay-ip=<公网IP>
min-port=49152
max-port=65535 # 可收窄;收窄即降低单机 allocation 上限
# ── 认证:时限凭据 ──────────────────────────
lt-cred-mech
use-auth-secret
static-auth-secret=<openssl rand -hex 32>
realm=t # 短 realm 可减小 401 响应体积(降低放大倍数)
stale-nonce=600
# ── TLS ───────────────────────────────────
cert=/etc/letsencrypt/live/turn.example.com/fullchain.pem
pkey=/etc/letsencrypt/live/turn.example.com/privkey.pem
cipher-list="HIGH"
# 注意:no-tlsv1 / no-tlsv1_1 在 4.7.0 已被移除,写了会报 Bad configuration format
# ── 访问控制(防中继滥用,第一优先级)────────
no-multicast-peers
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255 # 云元数据服务 169.254.169.254
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=224.0.0.0-239.255.255.255
denied-peer-ip=240.0.0.0-255.255.255.255
denied-peer-ip=::1
denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255 # 关键:堵 IPv4-mapped 绕过
denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
# 若媒体只发往自家 SFU,改用白名单最稳(Zoom 的做法):
# allowed-peer-ip=<SFU公网IP> + denied-peer-ip=0.0.0.0-255.255.255.255
# ── 协议瘦身 ───────────────────────────────
no-tcp-relay # WebRTC 只要 UDP 中继,关掉 RFC6062
# no-rfc5780 / no-software-attribute 在 4.7.0+ 已是默认,无需再写
unauthorized-ratelimit # 4.14.0+,限制 UDP 401 反射(实验性,先灰度)
unauthorized-ratelimit-rps=10
# ── 限额(按实际峰值给余量,别照抄)─────────
user-quota=100
total-quota=10000 # 全服务器上限:设太小 = 自制 DoS(错误 486)
max-allocate-lifetime=3600
max-bps=<单会话上限>
bps-capacity=<整机带宽上限>
# ── 运维面与进程 ───────────────────────────
no-cli # 管理面是历史 CVE 高发区,一律关
# web-admin 不要开
prometheus
proc-user=turnserver
proc-group=turnserver
log-file=/var/log/coturn/turnserver.log
syslog
# 端口:信令口 + 中继口,云安全组与主机防火墙两层都要开
ufw allow 3478/udp; ufw allow 3478/tcp
ufw allow 5349/udp; ufw allow 5349/tcp
ufw allow 443/tcp
ufw allow 49152:65535/udp # ← 忘了这条:allocate 成功、媒体不通
# /etc/sysctl.d/99-coturn.conf
net.core.rmem_max=26214400
net.core.wmem_max=26214400
net.core.netdev_max_backlog=65536
net.ipv4.udp_rmem_min=8192
net.ipv4.udp_wmem_min=8192
fs.file-max=2097152
# /etc/systemd/system/coturn.service.d/limits.conf
[Service]
LimitNOFILE=1048576
// username = 过期时间戳[:业务ID],credential = HMAC-SHA1(username, secret) 的 base64
function turnCredentials(secret, ttl = 12 * 3600) {
const username = `${Math.floor(Date.now()/1000) + ttl}:agent-42`;
const credential = crypto.createHmac('sha1', secret).update(username).digest('base64');
return { username, credential };
}
// 下发多个 URL 让 ICE 自行竞速:UDP 优先,TCP/TLS 443 兜底,再挂一个托管商做异地冗余
iceServers: [
{ urls: 'stun:turn-sg.example.com:3478' },
{ urls: ['turn:turn-sg.example.com:3478?transport=udp',
'turns:turn-sg.example.com:443?transport=tcp'], username, credential },
{ urls: 'turn:turn.cloudflare.com:3478', username: cfUser, credential: cfCred } // 兜底
]
注意:draft-uberti-behave-turn-rest-00 早已过期但仍是事实标准,coturn / eturnal / Cloudflare 都兼容。TTL 别给太长(Cloudflare 侧上限 48 小时),且必须服务端签发。
# 需要 nginx ≥1.11.5 且编入 stream_ssl_preread 模块(Jitsi 官方手册同款方案)
stream {
map $ssl_preread_server_name $backend {
turn.example.com 127.0.0.1:5349; # TURNS → coturn
default 127.0.0.1:8443; # 其它 → Web
}
server {
listen 443;
ssl_preread on;
proxy_pass $backend;
proxy_timeout 3600s; # 长通话别被默认超时切断
}
}
nginx 只读 ClientHello 的 SNI、不解密,因此不影响 DTLS/SRTP 端到端加密。若 TURN 独占一个 IP,直接 alt-tls-listening-port=443 更简单——但注意 443/UDP 也建议放开(可蹭 QUIC 放行策略)。
大量教程写"配 Redis 实现 coturn 集群"。实际上 coturn 的 Redis/MySQL/PostgreSQL 后端存的是用户库与状态上报,allocation(中继会话)本身不能跨节点漂移——媒体流的五元组绑在具体那台机器的那个端口上。所以:
_turn._udp / _turns._tcp SRV 记录。static-auth-secret(时限凭据机制下最省事)或共享数据库,保证凭据全局可用。为什么必须区域化:TURN 在媒体路径上,选错区域等于媒体绕地球。Anycast(Cloudflare)由 BGP 自动就近,自建做不到——ICEPerf 实测 Anycast 的 TURN 候选延迟 17.9 ms vs 固定 PoP 厂商 139.6 ms,就是这个差距的量化。自建的替代方案只能是"多区域 + 就近解析"。
| 攻击面 | 后果 | 对策 |
|---|---|---|
| 中继滥用 / 内网穿透(#1 威胁) | 拿到凭据者可用你的 TURN 访问内网服务、localhost、云元数据端点 169.254.169.254,或把你的服务器当 C2 跳板(Slack、Zoom 都出过真实案例) | 全私网/回环/链路本地 + ::ffff: 映射段 denied-peer-ip;能白名单就白名单(只允许自家 SFU IP) |
| 放大 / 反射 DDoS | 伪造源 IP 发未认证请求,401 响应(含 REALM/NONCE)比请求大约 4 倍,反射打受害者 | 短 realm;4.14.0+ 开 unauthorized-ratelimit;极端场景 no-udp 只留 TCP/TLS(代价是延迟);网络层 nftables 限速 + BCP38 |
| 凭据泄露 | 长期静态凭据一旦泄露即被白用流量 | 只用时限凭据(HMAC),TTL 数小时级;凭据 API 自身要限流鉴权 |
| 管理面漏洞(CVE 高发区) | Web 管理面出过认证绕过(任意错误密码可登录,2019→4.9.0 修)、存储型 XSS、SQL 注入(PostgreSQL 后端可达 OS 命令执行);CLI 出过任意文件写 | no-cli + 不开 web-admin;必须开就绑管理网 |
| 配额耗尽(自伤) | server 级 total-quota 设太小 → 一个人就能把全局分配打满,其他人全 486 | per-user 与 server 级配合;配额不能替代源 IP 级网络限速 |
| 版本 | 修复内容 |
|---|---|
| 4.15.0(2026-07-22) | GHSA-5538-7cxj-5jcc:MESSAGE-INTEGRITY 之后的 STUN 属性未按 RFC 8489 §9 忽略;mobility 会话恢复绑定原属主;多处溢出/截断修复 |
| 4.13.x(2026-06) | CVE-2026-53450(CVSS 7.4,::ffff:127.0.0.1 绕过默认回环防护)、CVE-2026-53449(CLI psd 任意文件写);4.13.1 起默认拒绝 link-local/ULA 中继并统一规范化 IPv4-in-IPv6 编码 |
| 4.12.0 / 4.11.0 | CVE-2026-53448(管理面 SQL 注入,CVSS 7.2)/CVE-2026-43915(管理面存储型 XSS) |
| 4.10.0 | CVE-2026-43994(CVSS 8.1,OAuth 令牌解码认证前栈溢出,仅 --oauth 模式) |
| 4.9.0 | CVE-2026-27624:::ffff:x.x.x.x 绕过全部 IPv4 denied-peer-ip(对 CVE-2020-26262 修复的绕过);管理面反向密码校验 |
显式写死 denied-peer-ip 范围的部署没被 CVE-2026-53450 影响——"不依赖默认值、显式声明 ACL"这一条纪律,实际挡掉了一个 CVSS 7.4。
total-quota=100:某高排名多区域教程里就这么写——这是全服务器只允许 100 路中继,第 101 个用户直接 486,等于自制 DoS。no-tlsv1 / no-tlsv1_1:4.7.0 起已移除(TLS 1.0/1.1 默认关),继续写会报 Bad configuration format;同理 no-rfc5780 / no-software-attribute 在 4.7.0+ 已是默认。denied-peer-ip=::0 绝不能写:::0 被当通配符匹配所有对端(含 IPv4),会把全部中继一刀砍死,所有版本通病。no-stun 的含义常被写错:它是"关闭 STUN Binding 服务",不是"要求 TURN 认证";关了之后这台机器就不能兼当 STUN 用。想反过来只做 STUN 用 stun-only。external-ip:云上 1:1 NAT 环境不设,XOR-RELAYED-ADDRESS 会返回私网地址,客户端拿到无用候选——K8s/云上最典型的失败。49152-65535/udp 必须在云安全组和主机防火墙两层都放行,否则"分配成功但没声音"。relay-ip:IPv6 对端将无法被中继。安全上是收益、功能上是限制,要明确取舍。stun.cloudflare.com(免费无限)做冗余。ss -tulpn | grep turnserver,3478/5349/443 齐全。turnutils_uclient -t -T -v -u USER -w PASS turn.example.com(TCP/TLS 分别跑);openssl s_client -connect turn.example.com:443 -servername turn.example.com 验 SNI 分流与证书链。iceTransportPolicy 设成 'relay',只有这样才真的在测 TURN(默认 'all' 时通常直连,测不到)。chrome://webrtc-internals 看选中候选是 relay / srflx / host,并把这个指标接进生产埋点。P0 的产出是一个数字:月中继 GB。它决定后面所有事。
user-quota/total-quota/max-bps/bps-capacity)按实测峰值 × 1.5 配置;评估 --multiplex-peer 与端口范围收窄。| 你的情况 | 选择 | 理由 |
|---|---|---|
| 纯 PSTN/SIP 外呼,没有 WebRTC 客户端 | 不需要 TURN | 服务器对服务器,先别建 |
| 音频网页座席,月中继 < 1 TB | Cloudflare TURN(Anycast,1 TB 免费) | 免费 + 延迟最优 + 零运维 |
| 视频客服 / 月中继 TB 级 | 自建 coturn(含流量套餐主机,新加坡/圣保罗双区) | 成本从千元级降到几十美元级 |
| 客户要求固定出口 IP 白名单 / 数据不出境 | 必须自建 | 托管商 IP 不固定、媒体经第三方 |
| 媒体服务器已在 K8s 私有网络 | STUNner(Gateway API) | 避开 hostNetwork 反模式 |
| 已用 LiveKit / Jitsi 全家桶 | 先用其内置 TURN | LiveKit 记得 443 与 use_external_ip;Jitsi 用 ssl_preread 复用 443 |
| Go 团队要深度定制认证/配额 | pion/turn v4 自建服务 | 逻辑完全可编程 |
媒体链路与自研架构:RTC 自研架构 ↗ / RTC 全球基础设施 ↗;编排框架与 wss 媒体流对接:语音 Agent 编排框架;把中继流量并入单通成本:全链路单通成本模型;PSTN 侧线路与合规:东南亚 / 拉美线路。