coturn一键部署:从参数配置到docker compose部署,搭建高可用WebRTC服务,理解各个端口的含义,实现加密
WebRTC 生产环境部署:coturn 服务器配置详解, 实现高性能 STUN/TURN 服务!想在生产环境中部署 WebRTC 应用, 却对 STUN/TURN 服务器配置感到头疼? 本文以开源项目 coturn 为例, 带你深入了解 STUN/TURN 协议、 端口配置、 用户鉴权、 Docker 部署等关键知识, 并提供生产环境集群化方案思路, 助你轻松构建高性能、 安全的 WebRTC 服务!
关键词: WebRTC, STUN, TURN, coturn, 服务器配置, 用户鉴权, Docker, 集群化, 高可用, 实时通信
上一篇文章中,我们系统的学习并整理了有关webRTC的基础。其中STUN服务的成本很低,因此使用的是Google STUN。
但是商业化的项目还是需要自己搭建STUN,同时国内网络环境限制,使得P2P的建连的成功率较低,TURN服务的引入也是必须的。因此我们本文的主角就是coturn这个服务,看下如何在生产环境中,引入并正确的配置部署coturn服务。
一、coturn的背景
随着实时通信应用(如VoIP、视频会议、在线游戏等)的普及,IETF(互联网工程任务组)制定了STUN和TURN的标准,来处理NAT穿透问题。
coturn作为一个开源项目应运而生,旨在提供一个高性能、功能丰富的STUN/TURN服务器实现。
GitHub地址:https://github.com/coturn/coturn
DockerHub:docker pull coturn/coturn
二、部署思路及核心的配置文件
2.1 coturn端口及协议的整体概览:
重点(‼️):
STUN/ICE消息通常不加密,即使在DTLS/TLS环境中。DTLS和TLS主要用于保护TURN中继的数据,而不是ICE/STUN消息。
点对点媒体流的加密通常由应用层处理(如WebRTC中的SRTP),与STUN和TURN无关。
客户端使用 UDP 连接到 coturn 的 UDP 3478 端口,coturn 会返回 UDP ICE 候选地址。客户端使用 TCP 连接到 coturn 的 TCP 3478 端口,coturn 会返回 TCP ICE 候选地址。
配置一个中继端口范围(例如49152-65535)用于媒体中继。中继端口范围内的端口可以用于 UDP 或 TCP 连接,协议类型取决于客户端的连接请求。例如,一个客户端可能使用 DTLS 连接到 coturn,而另一个客户端可能使用 TLS 连接到 coturn。
2.2用户及鉴权
当我们的服务仅仅提供STUN服务时,鉴权无意义。
但是,当我们引入TURN服务时,假如不进行必要的权限管控,就会涉及到数据安全的风险,任何人都可以连接到 TURN 端口,并尝试获取数据流。特别是当你应用层还未进行有效加密的情况下。
coturn支持多种类型的鉴权手段,其中最常见的就是long-term credential mechanism,coturn 支持两种lt-cred-mech:
plain mechanism: 用户名和密码以明文形式传输,安全性较低。
SHA1 mechanism: 用户名和密码使用 SHA1 哈希算法加密后传输,安全性较高。
同时还支持三方的鉴权( OAuth2.0/RADIUS等),配置用户数据库等等,可以结合项目的实际,选择符合业务安全等级的鉴权机制。
2.3现在可以看下核心配置文件了,
/etc/coturn/turnserver.conf ,已经过测试
# --- 网络配置 ---
# 监听所有网络接口。注意:在生产环境中,应该只监听必要的接口
listening-ip=0.0.0.0
# 标准 TURN 端口
listening-port=3478
# TLS/DTLS 端口(取消注释以启用)
#tls-listening-port=5349
#dtls-listening-port=5349
# --- 中继配置 ---
# 中继端口范围,根据您的网络环境和预期负载调整
min-port=49152
max-port=50000
# 内部中继IP地址
relay-ip=192.168.137.3
# 外部IP地址(NAT后的公网IP,如果有)
external-ip=192.168.137.3
# --- 认证配置 ---
# 设置域名,用于长期凭证机制
realm=example.com
# 启用长期凭证机制
lt-cred-mech
# --- 用户凭证 ---
# 直接在配置文件中定义用户。注意:在生产环境中应使用更安全的方法
user=user1:password1
user=user2:password2
# --- TLS/DTLS 配置 ---
# TLS 证书和私钥路径(取消注释以启用)
#cert=/etc/turnserver/fullchain.pem
#pkey=/etc/turnserver/privkey.pem
# 推荐的密码套件,提供强加密(取消注释以启用)
#cipher-list="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"
# --- 安全设置 ---
# 启用指纹,防止中间人攻击
fingerprint
# 启用过期 nonce 检测,防止重放攻击(取消注释以启用)
#stale-nonce=3600
# 设置 DTLS 会话密钥的生命周期(单位:秒)(取消注释以启用)
#dtls-key-lifetime=3600
# --- 性能优化 ---
# 最大允许的总带宽(字节/秒),0 表示无限制
max-bps=0
# 所有会话的总配额(字节/秒),格式:数字:数字,0 表示无限制
total-quota=0:0
# 单个用户的配额(字节/秒),0 表示无限制
user-quota=0
# --- 日志设置 ---
# 启用详细日志,便于调试。在生产环境中可以降低日志级别
verbose
# --- 高级配置 ---
# 允许环回地址,用于测试。生产环境中应禁用
#no-loopback-peers
# 允许使用 TURN 服务的 IP 范围,增强安全性(取消注释并根据需要调整)
#allowed-peer-ip=10.0.0.0-10.255.255.255
#allowed-peer-ip=172.16.0.0-172.31.255.255
#allowed-peer-ip=192.168.0.0-192.168.255.255
# 启用 CLI 访问和状态报告(取消注释并设置密码以启用)
#cli-password=<strong-admin-password>
#status-port=5986
# --- 注意事项 ---
# 1. 在生产环境中,确保所有密码和密钥都是强密码,并定期更新
# 2. 根据您的具体需求和网络环境调整配置
# 3. 定期检查日志文件,监控服务器性能和可能的安全问题
# 4. 确保 TLS 证书有效且定期更新
# 5. 考虑使用防火墙进一步限制对 TURN 服务器的访问
# 6. 在生产环境中,考虑使用外部认证系统而不是直接在配置文件中存储用户凭证
# 7. 根据实际负载调整性能相关的参数
# 8. 定期更新 TURN 服务器软件以获取最新的安全补丁
上述的鉴权信息是明文的,不安全。因此可以使用SHA1的机制进行加密,下边是一个说明,已经过测试:
1、服务端:
首先需要使用openssl的工具对于原本明文的部分进行加密,
例如:
echo -n "user1:example.com:password1" | openssl dgst -sha1
计算出来之后,在配置文件中:
# 启用 SHA1 认证机制
sha1-auth-enabled
# --- 用户凭证 ---
# 使用 SHA1 哈希后的密码
# 格式:user=username:SHA1(username:realm:password)
# 注意:您需要使用工具生成 SHA1 哈希值
user=user1:9e8e7b92799419c0032356ed361d13c7a7765d91
2、客户端:
客户端的代码也需要同步进行改造,
const config = {
iceServers: [
{ urls: 'stun:192.168.137.3:3478' },
{
urls: 'turn:192.168.137.3:3478',
username: 'user1',
credential: '9e8e7b92799419c0032356ed361d13c7a7765d91',
credentialType: 'password'
}
]
};
// 创建RTCPeerConnection
function createPeerConnection() {
const pc = new RTCPeerConnection(config);
return pc;
}
三、快速部署(docker)
docker-compose.yml 已经经过测试 ,其中映射的文件需要自己提前创建。
version: '3'
services:
coturn:
image: coturn/coturn:latest
container_name: coturn_server
restart: unless-stopped
network_mode: host # 使用主机网络模式以支持全范围的端口映射
volumes:
# 映射配置文件
- ./turnserver.conf:/etc/coturn/turnserver.conf:ro
# 映射 TLS 证书和私钥(如果使用)
- ./certs:/etc/coturn/certs:ro
3.1 完成整理,推送到github的项目地址,只需要简单修改就可以直接使用。
https://github.com/galtjay/coturn-docker-compose/
四、coturn的集群化方案思路
简化的思路:
四层反向代理:客户端初始请求发送至HAProxy(203.0.113.4:3478),HAProxy根据负载均衡策略将请求转发至某个TURN服务器(如203.0.113.1)。TURN服务器响应包含其自身IP,客户端随后直接与该TURN服务器通信,使用分配的临时端口进行数据中继。
Redis存储会话信息和认证数据:使得任何TURN实例都能处理任何会话,提高系统弹性。如遇高负载,TURN服务器可通过alternate-server机制重定向客户端到其他实例。整个过程实现了高效的负载均衡、会话持久性和动态调整,同时通过Redis确保了数据一致性和系统可扩展性。
整体而言,负载均衡依赖四层代理软件如HAProxy,对于coturn状态的检查也交给HAProxy。同时可以启动多个HAProxy,通过keepalived实现HAProxy的健康状态检查及故障转移,最终实现整个coturn的横向拓展以及高可用。至于redis的高可用,是另外一个话题,此处不再赘述。
案列配置,未经过测试:
# Redis配置 (redis.conf)
bind 192.168.1.10
port 6379
requirepass your_strong_redis_password
# COTURN实例1配置 (turnserver1.conf)
listening-ip=0.0.0.0
listening-port=3478
tls-listening-port=5349
relay-ip=203.0.113.1
external-ip=203.0.113.1
redis-statsdb="ip=192.168.1.10 port=6379 dbname=0 password=your_strong_redis_password"
realm=example.com
lt-cred-mech
userdb=/etc/turnserver/turndb.conf
alternate-server 203.0.113.2:3478
alternate-server 203.0.113.3:3478
prometheus
# COTURN实例2配置 (turnserver2.conf)
listening-ip=0.0.0.0
listening-port=3478
tls-listening-port=5349
relay-ip=203.0.113.2
external-ip=203.0.113.2
redis-statsdb="ip=192.168.1.10 port=6379 dbname=0 password=your_strong_redis_password"
realm=example.com
lt-cred-mech
userdb=/etc/turnserver/turndb.conf
alternate-server 203.0.113.1:3478
alternate-server 203.0.113.3:3478
prometheus
# COTURN实例3配置 (turnserver3.conf)
listening-ip=0.0.0.0
listening-port=3478
tls-listening-port=5349
relay-ip=203.0.113.3
external-ip=203.0.113.3
redis-statsdb="ip=192.168.1.10 port=6379 dbname=0 password=your_strong_redis_password"
realm=example.com
lt-cred-mech
userdb=/etc/turnserver/turndb.conf
alternate-server 203.0.113.1:3478
alternate-server 203.0.113.2:3478
prometheus
# HAProxy配置 (haproxy.cfg) 203.0.113.4
frontend turn_frontend
bind *:3478
mode tcp
default_backend turn_backend
backend turn_backend
mode tcp
balance roundrobin
option tcp-check
server turn1 203.0.113.1:3478 check
server turn2 203.0.113.2:3478 check
server turn3 203.0.113.3:3478 check
五、一些测试用的JS片段,在浏览器console中运行,不支持nodejs
5.1 测试STUN及TURN
// 配置信息
const config = {
iceServers: [
{ urls: 'stun:192.168.137.3:3478' },
{
urls: 'turn:192.168.137.3:3478',
username: 'user1',
credential: 'password1'
}
]
};
// 创建RTCPeerConnection
function createPeerConnection() {
const pc = new RTCPeerConnection(config);
return pc;
}
// 测试STUN服务器
function testSTUN() {
console.log('开始测试STUN服务器...');
const pc = createPeerConnection();
pc.onicecandidate = (event) => {
if (event.candidate) {
if (event.candidate.type === 'srflx') {
console.log('STUN测试成功!');
console.log('公网IP:', event.candidate.address);
console.log('公网端口:', event.candidate.port);
}
}
};
pc.createDataChannel('test');
pc.createOffer().then(offer => pc.setLocalDescription(offer));
setTimeout(() => {
if (!pc.localDescription) {
console.log('STUN测试失败:未能获取候选项');
}
pc.close();
}, 5000);
}
// 测试TURN服务器
function testTURN() {
console.log('开始测试TURN服务器...');
const pc = createPeerConnection();
pc.onicecandidate = (event) => {
if (event.candidate) {
if (event.candidate.type === 'relay') {
console.log('TURN测试成功!');
console.log('中继IP:', event.candidate.address);
console.log('中继端口:', event.candidate.port);
}
}
};
pc.createDataChannel('test');
pc.createOffer().then(offer => pc.setLocalDescription(offer));
setTimeout(() => {
if (!pc.localDescription) {
console.log('TURN测试失败:未能获取候选项');
}
pc.close();
}, 5000);
}
// 运行测试
testSTUN();
setTimeout(testTURN, 1000); // 等待STUN测试完成后运行TURN测试
5.2 单独调试TURN
// 配置信息
const config = {
iceServers: [
{ urls: 'stun:192.168.137.3:3478' },
{
urls: 'turn:192.168.137.3:3478',
username: 'user1',
credential: 'password1',
realm: 'example.com'
}
]
};
// 创建RTCPeerConnection
function createPeerConnection() {
const pc = new RTCPeerConnection(config);
return pc;
}
// 测试TURN服务器
async function testTURN() {
console.log('开始测试TURN服务器...');
const pc = createPeerConnection();
let hasRelayCandidate = false;
// 监听icecandidate事件
pc.onicecandidate = (event) => {
if (event.candidate) {
console.log('获取到ICE候选:', event.candidate.type, event.candidate.address);
if (event.candidate.type === 'relay') {
hasRelayCandidate = true;
console.log('TURN测试成功!');
console.log('中继IP:', event.candidate.address);
console.log('中继端口:', event.candidate.port);
}
}
};
// 创建DataChannel触发ICE协商
pc.createDataChannel('test');
// 创建Offer
try {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
console.log('已设置本地Offer:', offer);
// 等待一段时间,确保ICE协商完成
await new Promise(resolve => setTimeout(resolve, 10000));
if (!hasRelayCandidate) {
console.log('TURN测试失败:未能获取到Relay候选项');
}
} catch (error) {
console.error('TURN测试过程中发生错误:', error);
} finally {
pc.close();
console.log('TURN测试结束');
}
}
testTURN();
拓展阅读,WebRTC的基础: