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

传输

TURN

中继流量

点对点

媒体流

说明

UDP 3478 (STUN)

不加密

N/A

不加密

标准STUN,不含TURN

UDP 3478 (STUN/TURN)

不加密

不加密

不加密

基本TURN,无内置加密

TCP 3478 (STUN/TURN)

不加密

不加密

不加密

TCP版本的STUN/TURN

UDP 3478 (TURN over DTLS)

不加密

加密

不加密

DTLS加密TURN中继流量

TCP 5349 (TURN over TLS)

不加密

加密

不加密

TLS加密TURN中继流量

重点(‼️):

  • 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的基础:

https://cdn.watermelonwater.tech/archives/WebRTC%E5%AE%9E%E7%8E%B0%E8%AF%A6%E8%A7%A3%EF%BC%9A%E4%BB%8E%E5%88%9B%E5%BB%BAPeerConnection%E5%88%B0%E5%8F%91%E9%80%81RTC%20Session%20Desc%E7%9A%84%E5%85%B3%E9%94%AE%E6%AD%A5%E9%AA%A4%E5%92%8C%E5%AE%9E%E7%8E%B0%E7%BB%86%E8%8A%82%E4%BB%A5%E5%8F%8A%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5%E8%AF%A6%E8%A7%A3