121 lines
3.5 KiB
Go
121 lines
3.5 KiB
Go
|
|
/**
|
|||
|
|
* package turnserver
|
|||
|
|
* 作用:基于 pion/turn 封装内置的 WebRTC 中继服务。
|
|||
|
|
* 特性:
|
|||
|
|
* 1. 支持 UDP 和 TCP 协议。
|
|||
|
|
* 2. 手动实现了长期凭证机制 (Long-Term Credential Mechanism) 进行鉴权。
|
|||
|
|
* 3. 支持分布式部署,通过共享密钥验证 Token。
|
|||
|
|
*/
|
|||
|
|
package turnserver
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"crypto/hmac"
|
|||
|
|
"crypto/sha1"
|
|||
|
|
"encoding/base64"
|
|||
|
|
"fmt"
|
|||
|
|
"log"
|
|||
|
|
"net"
|
|||
|
|
"strconv"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/pion/turn/v2"
|
|||
|
|
"github.com/spf13/viper"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Start
|
|||
|
|
* 功能:初始化并启动 TURN 服务器。
|
|||
|
|
* 注意:这是一个阻塞调用,通常建议在 goroutine 中运行。
|
|||
|
|
*/
|
|||
|
|
func Start() {
|
|||
|
|
if !viper.GetBool("turn.enabled") {
|
|||
|
|
log.Println("ℹ️ [TURN] 服务已禁用")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
publicIP := viper.GetString("turn.public_ip")
|
|||
|
|
port := viper.GetInt("turn.listen_port")
|
|||
|
|
realm := viper.GetString("turn.realm")
|
|||
|
|
|
|||
|
|
// 监听 UDP 端口
|
|||
|
|
udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(port))
|
|||
|
|
if err != nil {
|
|||
|
|
log.Fatalf("❌ [TURN] UDP 监听失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 定义鉴权回调函数
|
|||
|
|
// 每次客户端发起 Allocate 请求时触发
|
|||
|
|
authHandler := func(username, realm string, srcAddr net.Addr) ([]byte, bool) {
|
|||
|
|
// 1. (可选) 验证 username 时效性
|
|||
|
|
// username 格式: timestamp:userid
|
|||
|
|
// 生产环境可以解析 timestamp 检查是否过期
|
|||
|
|
|
|||
|
|
// 2. 根据共享密钥重新计算密码 (HMAC-SHA1)
|
|||
|
|
secret := viper.GetString("turn.shared_secret")
|
|||
|
|
password := generateHMACPassword(username, secret)
|
|||
|
|
|
|||
|
|
// 3. 生成 HA1 Key
|
|||
|
|
// pion/turn 的 AuthHandler 需要返回 MD5(username:realm:password) 作为 Key
|
|||
|
|
// 这是 STUN/TURN 标准的完整性校验要求
|
|||
|
|
key := turn.GenerateAuthKey(username, realm, password)
|
|||
|
|
return key, true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建 TURN Server 实例
|
|||
|
|
s, err := turn.NewServer(turn.ServerConfig{
|
|||
|
|
Realm: realm,
|
|||
|
|
AuthHandler: authHandler,
|
|||
|
|
// 配置中继地址生成器 (使用公网IP,否则客户端无法连接)
|
|||
|
|
PacketConnConfigs: []turn.PacketConnConfig{
|
|||
|
|
{
|
|||
|
|
PacketConn: udpListener,
|
|||
|
|
RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
|
|||
|
|
RelayAddress: net.ParseIP(publicIP),
|
|||
|
|
Address: "0.0.0.0",
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if err != nil {
|
|||
|
|
log.Fatalf("❌ [TURN] 启动失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log.Printf("🚀 [TURN] 服务已启动 | IP: %s | Port: %d | Realm: %s", publicIP, port, realm)
|
|||
|
|
|
|||
|
|
// 阻塞保持服务运行
|
|||
|
|
// 在 main.go 中,我们依靠主线程的 select 或 Web Server 阻塞,所以这里不需要 block
|
|||
|
|
// 但为了代码独立性,如果是独立进程,需要阻塞。这里因为是库函数,只需返回即可,Server 会在后台运行。
|
|||
|
|
_ = s
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* GenerateCredentials
|
|||
|
|
* 功能:为特定用户生成 TURN 服务的访问凭证。
|
|||
|
|
* 算法:WebRTC 标准的 Long-Term Credential Mechanism (HMAC-SHA1)。
|
|||
|
|
* @param userID 用户ID
|
|||
|
|
* @return username, password (credential)
|
|||
|
|
*/
|
|||
|
|
func GenerateCredentials(userID string) (string, string) {
|
|||
|
|
// Username = timestamp:userid (有效期 24 小时)
|
|||
|
|
timestamp := time.Now().Add(24 * time.Hour).Unix()
|
|||
|
|
username := fmt.Sprintf("%d:%s", timestamp, userID)
|
|||
|
|
|
|||
|
|
secret := viper.GetString("turn.shared_secret")
|
|||
|
|
|
|||
|
|
// 生成密码
|
|||
|
|
password := generateHMACPassword(username, secret)
|
|||
|
|
return username, password
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* generateHMACPassword
|
|||
|
|
* 功能:辅助函数,使用 HMAC-SHA1 算法生成密码
|
|||
|
|
* 标准:Base64(HMAC-SHA1(secret, username))
|
|||
|
|
*/
|
|||
|
|
func generateHMACPassword(username, secret string) string {
|
|||
|
|
mac := hmac.New(sha1.New, []byte(secret))
|
|||
|
|
mac.Write([]byte(username))
|
|||
|
|
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|||
|
|
}
|