mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 04:33:00 +00:00
Bump version to 0.112.2 and update TURN server configuration
- Updated version in Makefile to 0.112.2. - Enhanced SFU server error handling to ignore http.ErrServerClosed. - Added TURNS (TURN over TLS) configuration options in TURN server and related components. - Updated firewall rules to include TURNS ports and modified related tests. - Implemented self-signed certificate generation for TURNS. - Adjusted TURN server to support both UDP and TCP listeners. - Updated WebRTC and SFU components to accommodate new TURNS configurations.
This commit is contained in:
parent
bcfdabb32d
commit
714a986a78
2
Makefile
2
Makefile
@ -63,7 +63,7 @@ test-e2e-quick:
|
||||
|
||||
.PHONY: build clean test deps tidy fmt vet lint install-hooks redeploy-devnet redeploy-testnet release health
|
||||
|
||||
VERSION := 0.112.1
|
||||
VERSION := 0.112.2
|
||||
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)'
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@ -35,7 +37,7 @@ func main() {
|
||||
|
||||
// Start HTTP server in background
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
logger.ComponentError(logging.ComponentSFU, "SFU server error", zap.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -40,14 +40,16 @@ func parseTURNConfig(logger *logging.ColoredLogger) *turn.Config {
|
||||
}
|
||||
|
||||
type yamlCfg struct {
|
||||
ListenAddr string `yaml:"listen_addr"`
|
||||
TLSListenAddr string `yaml:"tls_listen_addr"`
|
||||
PublicIP string `yaml:"public_ip"`
|
||||
Realm string `yaml:"realm"`
|
||||
AuthSecret string `yaml:"auth_secret"`
|
||||
RelayPortStart int `yaml:"relay_port_start"`
|
||||
RelayPortEnd int `yaml:"relay_port_end"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
ListenAddr string `yaml:"listen_addr"`
|
||||
TURNSListenAddr string `yaml:"turns_listen_addr"`
|
||||
PublicIP string `yaml:"public_ip"`
|
||||
Realm string `yaml:"realm"`
|
||||
AuthSecret string `yaml:"auth_secret"`
|
||||
RelayPortStart int `yaml:"relay_port_start"`
|
||||
RelayPortEnd int `yaml:"relay_port_end"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
TLSCertPath string `yaml:"tls_cert_path"`
|
||||
TLSKeyPath string `yaml:"tls_key_path"`
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
@ -66,14 +68,16 @@ func parseTURNConfig(logger *logging.ColoredLogger) *turn.Config {
|
||||
}
|
||||
|
||||
cfg := &turn.Config{
|
||||
ListenAddr: y.ListenAddr,
|
||||
TLSListenAddr: y.TLSListenAddr,
|
||||
PublicIP: y.PublicIP,
|
||||
Realm: y.Realm,
|
||||
AuthSecret: y.AuthSecret,
|
||||
RelayPortStart: y.RelayPortStart,
|
||||
RelayPortEnd: y.RelayPortEnd,
|
||||
Namespace: y.Namespace,
|
||||
ListenAddr: y.ListenAddr,
|
||||
TURNSListenAddr: y.TURNSListenAddr,
|
||||
PublicIP: y.PublicIP,
|
||||
Realm: y.Realm,
|
||||
AuthSecret: y.AuthSecret,
|
||||
RelayPortStart: y.RelayPortStart,
|
||||
RelayPortEnd: y.RelayPortEnd,
|
||||
Namespace: y.Namespace,
|
||||
TLSCertPath: y.TLSCertPath,
|
||||
TLSKeyPath: y.TLSKeyPath,
|
||||
}
|
||||
|
||||
if errs := cfg.Validate(); len(errs) > 0 {
|
||||
|
||||
@ -12,7 +12,7 @@ type FirewallConfig struct {
|
||||
IsNameserver bool // enables port 53 TCP+UDP
|
||||
AnyoneORPort int // 0 = disabled, typically 9001
|
||||
WireGuardPort int // default 51820
|
||||
TURNEnabled bool // enables TURN relay ports (3478/udp, 443/udp, relay range)
|
||||
TURNEnabled bool // enables TURN relay ports (3478/udp+tcp, 5349/tcp, relay range)
|
||||
TURNRelayStart int // start of TURN relay port range (default 49152)
|
||||
TURNRelayEnd int // end of TURN relay port range (default 65535)
|
||||
}
|
||||
@ -89,8 +89,9 @@ func (fp *FirewallProvisioner) GenerateRules() []string {
|
||||
|
||||
// TURN relay (only for nodes running TURN servers)
|
||||
if fp.config.TURNEnabled {
|
||||
rules = append(rules, "ufw allow 3478/udp") // TURN standard port
|
||||
rules = append(rules, "ufw allow 443/udp") // TURN TLS port (does not conflict with Caddy TCP 443)
|
||||
rules = append(rules, "ufw allow 3478/udp") // TURN standard port (UDP)
|
||||
rules = append(rules, "ufw allow 3478/tcp") // TURN standard port (TCP fallback)
|
||||
rules = append(rules, "ufw allow 5349/tcp") // TURNS (TURN over TLS/TCP)
|
||||
if fp.config.TURNRelayStart > 0 && fp.config.TURNRelayEnd > 0 {
|
||||
rules = append(rules, fmt.Sprintf("ufw allow %d:%d/udp", fp.config.TURNRelayStart, fp.config.TURNRelayEnd))
|
||||
}
|
||||
@ -147,7 +148,8 @@ func (fp *FirewallProvisioner) IsActive() bool {
|
||||
func (fp *FirewallProvisioner) AddWebRTCRules(relayStart, relayEnd int) error {
|
||||
rules := []string{
|
||||
"ufw allow 3478/udp",
|
||||
"ufw allow 443/udp",
|
||||
"ufw allow 3478/tcp",
|
||||
"ufw allow 5349/tcp",
|
||||
}
|
||||
if relayStart > 0 && relayEnd > 0 {
|
||||
rules = append(rules, fmt.Sprintf("ufw allow %d:%d/udp", relayStart, relayEnd))
|
||||
@ -168,7 +170,8 @@ func (fp *FirewallProvisioner) AddWebRTCRules(relayStart, relayEnd int) error {
|
||||
func (fp *FirewallProvisioner) RemoveWebRTCRules(relayStart, relayEnd int) error {
|
||||
rules := []string{
|
||||
"ufw delete allow 3478/udp",
|
||||
"ufw delete allow 443/udp",
|
||||
"ufw delete allow 3478/tcp",
|
||||
"ufw delete allow 5349/tcp",
|
||||
}
|
||||
if relayStart > 0 && relayEnd > 0 {
|
||||
rules = append(rules, fmt.Sprintf("ufw delete allow %d:%d/udp", relayStart, relayEnd))
|
||||
|
||||
@ -96,6 +96,21 @@ func TestFirewallProvisioner_GenerateRules_FullConfig(t *testing.T) {
|
||||
assertContainsRule(t, rules, "ufw allow 9001/tcp")
|
||||
}
|
||||
|
||||
func TestFirewallProvisioner_GenerateRules_WithTURN(t *testing.T) {
|
||||
fp := NewFirewallProvisioner(FirewallConfig{
|
||||
TURNEnabled: true,
|
||||
TURNRelayStart: 49152,
|
||||
TURNRelayEnd: 49951,
|
||||
})
|
||||
|
||||
rules := fp.GenerateRules()
|
||||
|
||||
assertContainsRule(t, rules, "ufw allow 3478/udp")
|
||||
assertContainsRule(t, rules, "ufw allow 3478/tcp")
|
||||
assertContainsRule(t, rules, "ufw allow 5349/tcp")
|
||||
assertContainsRule(t, rules, "ufw allow 49152:49951/udp")
|
||||
}
|
||||
|
||||
func TestFirewallProvisioner_DefaultPorts(t *testing.T) {
|
||||
fp := NewFirewallProvisioner(FirewallConfig{})
|
||||
|
||||
|
||||
@ -65,7 +65,7 @@ type SpawnRequest struct {
|
||||
|
||||
// TURN config (when action = "spawn-turn")
|
||||
TURNListenAddr string `json:"turn_listen_addr,omitempty"`
|
||||
TURNTLSAddr string `json:"turn_tls_addr,omitempty"`
|
||||
TURNTURNSAddr string `json:"turn_turns_addr,omitempty"`
|
||||
TURNPublicIP string `json:"turn_public_ip,omitempty"`
|
||||
TURNRealm string `json:"turn_realm,omitempty"`
|
||||
TURNAuthSecret string `json:"turn_auth_secret,omitempty"`
|
||||
@ -347,7 +347,7 @@ func (h *SpawnHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
Namespace: req.Namespace,
|
||||
NodeID: req.NodeID,
|
||||
ListenAddr: req.TURNListenAddr,
|
||||
TLSListenAddr: req.TURNTLSAddr,
|
||||
TURNSListenAddr: req.TURNTURNSAddr,
|
||||
PublicIP: req.TURNPublicIP,
|
||||
Realm: req.TURNRealm,
|
||||
AuthSecret: req.TURNAuthSecret,
|
||||
|
||||
@ -38,7 +38,8 @@ func (h *WebRTCHandlers) CredentialsHandler(w http.ResponseWriter, r *http.Reque
|
||||
if h.turnDomain != "" {
|
||||
uris = append(uris,
|
||||
fmt.Sprintf("turn:%s:3478?transport=udp", h.turnDomain),
|
||||
fmt.Sprintf("turn:%s:443?transport=udp", h.turnDomain),
|
||||
fmt.Sprintf("turn:%s:3478?transport=tcp", h.turnDomain),
|
||||
fmt.Sprintf("turns:%s:5349", h.turnDomain),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -61,8 +61,8 @@ func TestCredentialsHandler_Success(t *testing.T) {
|
||||
t.Errorf("ttl = %v, want 600", result["ttl"])
|
||||
}
|
||||
uris, ok := result["uris"].([]interface{})
|
||||
if !ok || len(uris) != 2 {
|
||||
t.Errorf("uris count = %v, want 2", result["uris"])
|
||||
if !ok || len(uris) != 3 {
|
||||
t.Errorf("uris count = %v, want 3", result["uris"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1956,15 +1956,15 @@ func (cm *ClusterManager) restoreClusterFromState(ctx context.Context, state *Cl
|
||||
webrtcCfg, err := cm.GetWebRTCConfig(ctx, state.NamespaceName)
|
||||
if err == nil && webrtcCfg != nil {
|
||||
turnCfg := TURNInstanceConfig{
|
||||
Namespace: state.NamespaceName,
|
||||
NodeID: cm.localNodeID,
|
||||
ListenAddr: fmt.Sprintf("0.0.0.0:%d", state.TURNListenPort),
|
||||
TLSListenAddr: fmt.Sprintf("0.0.0.0:%d", state.TURNTLSPort),
|
||||
PublicIP: "", // Will be resolved by spawner or from node info
|
||||
Realm: cm.baseDomain,
|
||||
AuthSecret: webrtcCfg.TURNSharedSecret,
|
||||
RelayPortStart: state.TURNRelayPortStart,
|
||||
RelayPortEnd: state.TURNRelayPortEnd,
|
||||
Namespace: state.NamespaceName,
|
||||
NodeID: cm.localNodeID,
|
||||
ListenAddr: fmt.Sprintf("0.0.0.0:%d", state.TURNListenPort),
|
||||
TURNSListenAddr: fmt.Sprintf("0.0.0.0:%d", state.TURNTLSPort),
|
||||
PublicIP: "", // Will be resolved by spawner or from node info
|
||||
Realm: cm.baseDomain,
|
||||
AuthSecret: webrtcCfg.TURNSharedSecret,
|
||||
RelayPortStart: state.TURNRelayPortStart,
|
||||
RelayPortEnd: state.TURNRelayPortEnd,
|
||||
}
|
||||
if err := cm.systemdSpawner.SpawnTURN(ctx, state.NamespaceName, cm.localNodeID, turnCfg); err != nil {
|
||||
cm.logger.Error("Failed to restore TURN from state", zap.String("namespace", state.NamespaceName), zap.Error(err))
|
||||
@ -1992,8 +1992,8 @@ func (cm *ClusterManager) restoreClusterFromState(ctx context.Context, state *Cl
|
||||
MediaPortStart: state.SFUMediaPortStart,
|
||||
MediaPortEnd: state.SFUMediaPortEnd,
|
||||
TURNServers: []sfu.TURNServerConfig{
|
||||
{Host: turnDomain, Port: TURNDefaultPort},
|
||||
{Host: turnDomain, Port: TURNTLSPort},
|
||||
{Host: turnDomain, Port: TURNDefaultPort, Secure: false},
|
||||
{Host: turnDomain, Port: TURNSPort, Secure: true},
|
||||
},
|
||||
TURNSecret: webrtcCfg.TURNSharedSecret,
|
||||
TURNCredTTL: webrtcCfg.TURNCredentialTTL,
|
||||
|
||||
@ -102,8 +102,8 @@ func (cm *ClusterManager) EnableWebRTC(ctx context.Context, namespaceName, enabl
|
||||
// 9. Build TURN server list for SFU config
|
||||
turnDomain := fmt.Sprintf("turn.ns-%s.%s", namespaceName, cm.baseDomain)
|
||||
turnServers := []sfu.TURNServerConfig{
|
||||
{Host: turnDomain, Port: TURNDefaultPort},
|
||||
{Host: turnDomain, Port: TURNTLSPort},
|
||||
{Host: turnDomain, Port: TURNDefaultPort, Secure: false},
|
||||
{Host: turnDomain, Port: TURNSPort, Secure: true},
|
||||
}
|
||||
|
||||
// 10. Get port blocks for RQLite DSN
|
||||
@ -123,15 +123,15 @@ func (cm *ClusterManager) EnableWebRTC(ctx context.Context, namespaceName, enabl
|
||||
for _, node := range turnNodes {
|
||||
turnBlock := turnBlocks[node.NodeID]
|
||||
turnCfg := TURNInstanceConfig{
|
||||
Namespace: namespaceName,
|
||||
NodeID: node.NodeID,
|
||||
ListenAddr: fmt.Sprintf("0.0.0.0:%d", turnBlock.TURNListenPort),
|
||||
TLSListenAddr: fmt.Sprintf("0.0.0.0:%d", turnBlock.TURNTLSPort),
|
||||
PublicIP: node.PublicIP,
|
||||
Realm: cm.baseDomain,
|
||||
AuthSecret: turnSecret,
|
||||
RelayPortStart: turnBlock.TURNRelayPortStart,
|
||||
RelayPortEnd: turnBlock.TURNRelayPortEnd,
|
||||
Namespace: namespaceName,
|
||||
NodeID: node.NodeID,
|
||||
ListenAddr: fmt.Sprintf("0.0.0.0:%d", turnBlock.TURNListenPort),
|
||||
TURNSListenAddr: fmt.Sprintf("0.0.0.0:%d", turnBlock.TURNTLSPort),
|
||||
PublicIP: node.PublicIP,
|
||||
Realm: cm.baseDomain,
|
||||
AuthSecret: turnSecret,
|
||||
RelayPortStart: turnBlock.TURNRelayPortStart,
|
||||
RelayPortEnd: turnBlock.TURNRelayPortEnd,
|
||||
}
|
||||
|
||||
if err := cm.spawnTURNOnNode(ctx, node, namespaceName, turnCfg); err != nil {
|
||||
@ -184,9 +184,11 @@ func (cm *ClusterManager) EnableWebRTC(ctx context.Context, namespaceName, enabl
|
||||
turnIPs = append(turnIPs, node.PublicIP)
|
||||
}
|
||||
if err := cm.dnsManager.CreateTURNRecords(ctx, namespaceName, turnIPs); err != nil {
|
||||
cm.logger.Warn("Failed to create TURN DNS records",
|
||||
cm.logger.Error("Failed to create TURN DNS records, aborting WebRTC enablement",
|
||||
zap.String("namespace", namespaceName),
|
||||
zap.Error(err))
|
||||
cm.cleanupWebRTCOnError(ctx, cluster.ID, namespaceName, clusterNodes)
|
||||
return fmt.Errorf("failed to create TURN DNS records: %w", err)
|
||||
}
|
||||
|
||||
// 14. Update cluster-state.json on all nodes with WebRTC info
|
||||
@ -438,8 +440,9 @@ func (cm *ClusterManager) spawnSFURemote(ctx context.Context, nodeIP string, cfg
|
||||
turnServers := make([]map[string]interface{}, len(cfg.TURNServers))
|
||||
for i, ts := range cfg.TURNServers {
|
||||
turnServers[i] = map[string]interface{}{
|
||||
"host": ts.Host,
|
||||
"port": ts.Port,
|
||||
"host": ts.Host,
|
||||
"port": ts.Port,
|
||||
"secure": ts.Secure,
|
||||
}
|
||||
}
|
||||
|
||||
@ -465,7 +468,7 @@ func (cm *ClusterManager) spawnTURNRemote(ctx context.Context, nodeIP string, cf
|
||||
"namespace": cfg.Namespace,
|
||||
"node_id": cfg.NodeID,
|
||||
"turn_listen_addr": cfg.ListenAddr,
|
||||
"turn_tls_addr": cfg.TLSListenAddr,
|
||||
"turn_turns_addr": cfg.TURNSListenAddr,
|
||||
"turn_public_ip": cfg.PublicIP,
|
||||
"turn_realm": cfg.Realm,
|
||||
"turn_auth_secret": cfg.AuthSecret,
|
||||
|
||||
@ -61,7 +61,7 @@ func (drm *DNSRecordManager) CreateNamespaceRecords(ctx context.Context, namespa
|
||||
insertQuery := `
|
||||
INSERT INTO dns_records (
|
||||
id, fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, TRUE, ?, ?)
|
||||
`
|
||||
now := time.Now()
|
||||
_, err := drm.db.Exec(internalCtx, insertQuery,
|
||||
@ -72,7 +72,6 @@ func (drm *DNSRecordManager) CreateNamespaceRecords(ctx context.Context, namespa
|
||||
60, // 60 second TTL for quick failover
|
||||
"namespace:"+namespaceName, // Track ownership with namespace prefix
|
||||
"cluster-manager", // Created by the cluster manager
|
||||
true, // Active
|
||||
now,
|
||||
now,
|
||||
)
|
||||
@ -96,7 +95,7 @@ func (drm *DNSRecordManager) CreateNamespaceRecords(ctx context.Context, namespa
|
||||
insertQuery := `
|
||||
INSERT INTO dns_records (
|
||||
id, fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, TRUE, ?, ?)
|
||||
`
|
||||
now := time.Now()
|
||||
_, err := drm.db.Exec(internalCtx, insertQuery,
|
||||
@ -107,7 +106,6 @@ func (drm *DNSRecordManager) CreateNamespaceRecords(ctx context.Context, namespa
|
||||
60,
|
||||
"namespace:"+namespaceName,
|
||||
"cluster-manager",
|
||||
true,
|
||||
now,
|
||||
now,
|
||||
)
|
||||
@ -230,11 +228,11 @@ func (drm *DNSRecordManager) AddNamespaceRecord(ctx context.Context, namespaceNa
|
||||
insertQuery := `
|
||||
INSERT INTO dns_records (
|
||||
id, fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, TRUE, ?, ?)
|
||||
`
|
||||
_, err := drm.db.Exec(internalCtx, insertQuery,
|
||||
recordID, f, "A", ip, 60,
|
||||
"namespace:"+namespaceName, "cluster-manager", true, now, now,
|
||||
"namespace:"+namespaceName, "cluster-manager", now, now,
|
||||
)
|
||||
if err != nil {
|
||||
return &ClusterError{
|
||||
@ -328,13 +326,13 @@ func (drm *DNSRecordManager) CreateTURNRecords(ctx context.Context, namespaceNam
|
||||
insertQuery := `
|
||||
INSERT INTO dns_records (
|
||||
id, fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, TRUE, ?, ?)
|
||||
`
|
||||
_, err := drm.db.Exec(internalCtx, insertQuery,
|
||||
recordID, fqdn, "A", ip, 60,
|
||||
"namespace-turn:"+namespaceName,
|
||||
"cluster-manager",
|
||||
true, now, now,
|
||||
now, now,
|
||||
)
|
||||
if err != nil {
|
||||
return &ClusterError{
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
production "github.com/DeBrosOfficial/network/pkg/environments/production"
|
||||
"github.com/DeBrosOfficial/network/pkg/gateway"
|
||||
"github.com/DeBrosOfficial/network/pkg/olric"
|
||||
"github.com/DeBrosOfficial/network/pkg/rqlite"
|
||||
@ -392,15 +393,15 @@ func (s *SystemdSpawner) StopSFU(ctx context.Context, namespace, nodeID string)
|
||||
|
||||
// TURNInstanceConfig holds configuration for spawning a TURN instance
|
||||
type TURNInstanceConfig struct {
|
||||
Namespace string
|
||||
NodeID string
|
||||
ListenAddr string // e.g., "0.0.0.0:3478"
|
||||
TLSListenAddr string // e.g., "0.0.0.0:443" (UDP, no conflict with Caddy TCP)
|
||||
PublicIP string // Public IP for TURN relay allocations
|
||||
Realm string // TURN realm (typically base domain)
|
||||
AuthSecret string // HMAC-SHA1 shared secret
|
||||
RelayPortStart int // Start of relay port range
|
||||
RelayPortEnd int // End of relay port range
|
||||
Namespace string
|
||||
NodeID string
|
||||
ListenAddr string // e.g., "0.0.0.0:3478"
|
||||
TURNSListenAddr string // e.g., "0.0.0.0:5349" (TURNS over TLS/TCP)
|
||||
PublicIP string // Public IP for TURN relay allocations
|
||||
Realm string // TURN realm (typically base domain)
|
||||
AuthSecret string // HMAC-SHA1 shared secret
|
||||
RelayPortStart int // Start of relay port range
|
||||
RelayPortEnd int // End of relay port range
|
||||
}
|
||||
|
||||
// SpawnTURN starts a TURN instance using systemd
|
||||
@ -419,16 +420,38 @@ func (s *SystemdSpawner) SpawnTURN(ctx context.Context, namespace, nodeID string
|
||||
|
||||
configPath := filepath.Join(configDir, fmt.Sprintf("turn-%s.yaml", nodeID))
|
||||
|
||||
// Generate self-signed TLS cert for TURNS if not already present
|
||||
certPath := filepath.Join(configDir, "turn-cert.pem")
|
||||
keyPath := filepath.Join(configDir, "turn-key.pem")
|
||||
if cfg.TURNSListenAddr != "" {
|
||||
if _, err := os.Stat(certPath); os.IsNotExist(err) {
|
||||
if err := turn.GenerateSelfSignedCert(certPath, keyPath, cfg.PublicIP); err != nil {
|
||||
s.logger.Warn("Failed to generate TURNS self-signed cert, TURNS will be disabled",
|
||||
zap.String("namespace", namespace),
|
||||
zap.Error(err))
|
||||
cfg.TURNSListenAddr = "" // Disable TURNS if cert generation fails
|
||||
} else {
|
||||
s.logger.Info("Generated TURNS self-signed certificate",
|
||||
zap.String("namespace", namespace),
|
||||
zap.String("cert_path", certPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build TURN YAML config
|
||||
turnConfig := turn.Config{
|
||||
ListenAddr: cfg.ListenAddr,
|
||||
TLSListenAddr: cfg.TLSListenAddr,
|
||||
PublicIP: cfg.PublicIP,
|
||||
Realm: cfg.Realm,
|
||||
AuthSecret: cfg.AuthSecret,
|
||||
RelayPortStart: cfg.RelayPortStart,
|
||||
RelayPortEnd: cfg.RelayPortEnd,
|
||||
Namespace: cfg.Namespace,
|
||||
ListenAddr: cfg.ListenAddr,
|
||||
TURNSListenAddr: cfg.TURNSListenAddr,
|
||||
PublicIP: cfg.PublicIP,
|
||||
Realm: cfg.Realm,
|
||||
AuthSecret: cfg.AuthSecret,
|
||||
RelayPortStart: cfg.RelayPortStart,
|
||||
RelayPortEnd: cfg.RelayPortEnd,
|
||||
Namespace: cfg.Namespace,
|
||||
}
|
||||
if cfg.TURNSListenAddr != "" {
|
||||
turnConfig.TLSCertPath = certPath
|
||||
turnConfig.TLSKeyPath = keyPath
|
||||
}
|
||||
|
||||
configBytes, err := yaml.Marshal(turnConfig)
|
||||
@ -464,6 +487,14 @@ func (s *SystemdSpawner) SpawnTURN(ctx context.Context, namespace, nodeID string
|
||||
return fmt.Errorf("TURN service did not become active: %w", err)
|
||||
}
|
||||
|
||||
// Add firewall rules for TURN ports
|
||||
fw := production.NewFirewallProvisioner(production.FirewallConfig{})
|
||||
if err := fw.AddWebRTCRules(cfg.RelayPortStart, cfg.RelayPortEnd); err != nil {
|
||||
s.logger.Warn("Failed to add WebRTC firewall rules (TURN service is running)",
|
||||
zap.String("namespace", namespace),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("TURN spawned successfully via systemd",
|
||||
zap.String("namespace", namespace),
|
||||
zap.String("node_id", nodeID))
|
||||
@ -477,7 +508,17 @@ func (s *SystemdSpawner) StopTURN(ctx context.Context, namespace, nodeID string)
|
||||
zap.String("namespace", namespace),
|
||||
zap.String("node_id", nodeID))
|
||||
|
||||
return s.systemdMgr.StopService(namespace, systemd.ServiceTypeTURN)
|
||||
err := s.systemdMgr.StopService(namespace, systemd.ServiceTypeTURN)
|
||||
|
||||
// Remove firewall rules for standard TURN ports
|
||||
fw := production.NewFirewallProvisioner(production.FirewallConfig{})
|
||||
if fwErr := fw.RemoveWebRTCRules(0, 0); fwErr != nil {
|
||||
s.logger.Warn("Failed to remove WebRTC firewall rules",
|
||||
zap.String("namespace", namespace),
|
||||
zap.Error(fwErr))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// SaveClusterState writes cluster state JSON to the namespace data directory.
|
||||
|
||||
@ -110,8 +110,8 @@ const (
|
||||
TURNRelayPortsPerNamespace = 800
|
||||
|
||||
// TURN listen ports (standard)
|
||||
TURNDefaultPort = 3478
|
||||
TURNTLSPort = 443
|
||||
TURNDefaultPort = 3478
|
||||
TURNSPort = 5349 // TURNS (TURN over TLS on TCP)
|
||||
|
||||
// Default TURN credential TTL in seconds (10 minutes)
|
||||
DefaultTURNCredentialTTL = 600
|
||||
|
||||
@ -217,7 +217,7 @@ func (wpa *WebRTCPortAllocator) tryAllocateTURNPorts(ctx context.Context, nodeID
|
||||
NamespaceClusterID: namespaceClusterID,
|
||||
ServiceType: "turn",
|
||||
TURNListenPort: TURNDefaultPort,
|
||||
TURNTLSPort: TURNTLSPort,
|
||||
TURNTLSPort: TURNSPort,
|
||||
TURNRelayPortStart: relayStart,
|
||||
TURNRelayPortEnd: relayStart + TURNRelayPortsPerNamespace - 1,
|
||||
AllocatedAt: time.Now(),
|
||||
|
||||
@ -168,8 +168,8 @@ func TestWebRTCPortAllocator_AllocateTURNPorts(t *testing.T) {
|
||||
if block.TURNListenPort != TURNDefaultPort {
|
||||
t.Errorf("TURNListenPort = %d, want %d", block.TURNListenPort, TURNDefaultPort)
|
||||
}
|
||||
if block.TURNTLSPort != TURNTLSPort {
|
||||
t.Errorf("TURNTLSPort = %d, want %d", block.TURNTLSPort, TURNTLSPort)
|
||||
if block.TURNTLSPort != TURNSPort {
|
||||
t.Errorf("TURNTLSPort = %d, want %d", block.TURNTLSPort, TURNSPort)
|
||||
}
|
||||
if block.TURNRelayPortStart != TURNRelayPortRangeStart {
|
||||
t.Errorf("TURNRelayPortStart = %d, want %d", block.TURNRelayPortStart, TURNRelayPortRangeStart)
|
||||
@ -320,7 +320,7 @@ func TestWebRTCPortBlock_TURNFields(t *testing.T) {
|
||||
NamespaceClusterID: "cluster-1",
|
||||
ServiceType: "turn",
|
||||
TURNListenPort: 3478,
|
||||
TURNTLSPort: 443,
|
||||
TURNTLSPort: 5349,
|
||||
TURNRelayPortStart: 49152,
|
||||
TURNRelayPortEnd: 49951,
|
||||
}
|
||||
|
||||
@ -30,8 +30,9 @@ type Config struct {
|
||||
|
||||
// TURNServerConfig represents a single TURN server endpoint
|
||||
type TURNServerConfig struct {
|
||||
Host string `yaml:"host"` // IP or hostname
|
||||
Port int `yaml:"port"` // UDP port (3478 or 443)
|
||||
Host string `yaml:"host"` // IP or hostname
|
||||
Port int `yaml:"port"` // Port number (3478 for TURN, 5349 for TURNS)
|
||||
Secure bool `yaml:"secure"` // true = TURNS (TLS over TCP), false = TURN (UDP)
|
||||
}
|
||||
|
||||
// Validate checks the SFU configuration for errors
|
||||
|
||||
@ -539,7 +539,12 @@ func (r *Room) buildICEServers() []webrtc.ICEServer {
|
||||
|
||||
var urls []string
|
||||
for _, ts := range r.config.TURNServers {
|
||||
urls = append(urls, fmt.Sprintf("turn:%s:%d?transport=udp", ts.Host, ts.Port))
|
||||
if ts.Secure {
|
||||
urls = append(urls, fmt.Sprintf("turns:%s:%d", ts.Host, ts.Port))
|
||||
} else {
|
||||
urls = append(urls, fmt.Sprintf("turn:%s:%d?transport=udp", ts.Host, ts.Port))
|
||||
urls = append(urls, fmt.Sprintf("turn:%s:%d?transport=tcp", ts.Host, ts.Port))
|
||||
}
|
||||
}
|
||||
|
||||
ttl := time.Duration(r.config.TURNCredentialTTL) * time.Second
|
||||
|
||||
@ -179,11 +179,14 @@ func TestRoomBuildICEServers(t *testing.T) {
|
||||
if len(servers) != 1 {
|
||||
t.Fatalf("ICE servers count = %d, want 1", len(servers))
|
||||
}
|
||||
if len(servers[0].URLs) != 1 {
|
||||
t.Fatalf("URLs count = %d, want 1", len(servers[0].URLs))
|
||||
if len(servers[0].URLs) != 2 {
|
||||
t.Fatalf("URLs count = %d, want 2", len(servers[0].URLs))
|
||||
}
|
||||
if servers[0].URLs[0] != "turn:1.2.3.4:3478?transport=udp" {
|
||||
t.Errorf("URL = %q, want %q", servers[0].URLs[0], "turn:1.2.3.4:3478?transport=udp")
|
||||
t.Errorf("URL[0] = %q, want %q", servers[0].URLs[0], "turn:1.2.3.4:3478?transport=udp")
|
||||
}
|
||||
if servers[0].URLs[1] != "turn:1.2.3.4:3478?transport=tcp" {
|
||||
t.Errorf("URL[1] = %q, want %q", servers[0].URLs[1], "turn:1.2.3.4:3478?transport=tcp")
|
||||
}
|
||||
if servers[0].Username == "" {
|
||||
t.Error("Username should not be empty")
|
||||
@ -222,8 +225,8 @@ func TestRoomBuildICEServersNoSecret(t *testing.T) {
|
||||
func TestRoomBuildICEServersMultipleTURN(t *testing.T) {
|
||||
cfg := testConfig()
|
||||
cfg.TURNServers = []TURNServerConfig{
|
||||
{Host: "1.2.3.4", Port: 3478},
|
||||
{Host: "5.6.7.8", Port: 443},
|
||||
{Host: "1.2.3.4", Port: 3478}, // non-secure → UDP + TCP = 2 URIs
|
||||
{Host: "5.6.7.8", Port: 5349, Secure: true}, // secure → 1 URI
|
||||
}
|
||||
|
||||
rm := NewRoomManager(cfg, testLogger())
|
||||
@ -233,8 +236,9 @@ func TestRoomBuildICEServersMultipleTURN(t *testing.T) {
|
||||
if len(servers) != 1 {
|
||||
t.Fatalf("ICE servers count = %d, want 1", len(servers))
|
||||
}
|
||||
if len(servers[0].URLs) != 2 {
|
||||
t.Fatalf("URLs count = %d, want 2", len(servers[0].URLs))
|
||||
// 1 non-secure (UDP+TCP) + 1 secure (TURNS) = 3 URIs
|
||||
if len(servers[0].URLs) != 3 {
|
||||
t.Fatalf("URLs count = %d, want 3", len(servers[0].URLs))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -257,7 +257,12 @@ func (s *Server) sendTURNCredentials(peer *Peer) {
|
||||
|
||||
var uris []string
|
||||
for _, ts := range s.config.TURNServers {
|
||||
uris = append(uris, fmt.Sprintf("turn:%s:%d?transport=udp", ts.Host, ts.Port))
|
||||
if ts.Secure {
|
||||
uris = append(uris, fmt.Sprintf("turns:%s:%d", ts.Host, ts.Port))
|
||||
} else {
|
||||
uris = append(uris, fmt.Sprintf("turn:%s:%d?transport=udp", ts.Host, ts.Port))
|
||||
uris = append(uris, fmt.Sprintf("turn:%s:%d?transport=tcp", ts.Host, ts.Port))
|
||||
}
|
||||
}
|
||||
|
||||
peer.SendMessage(NewServerMessage(MessageTypeTURNCredentials, &TURNCredentialsData{
|
||||
|
||||
@ -233,7 +233,7 @@ func TestTURNCredentialsDataSerialization(t *testing.T) {
|
||||
Username: "1234567890:test-ns",
|
||||
Password: "base64password==",
|
||||
TTL: 600,
|
||||
URIs: []string{"turn:1.2.3.4:3478?transport=udp", "turn:5.6.7.8:443?transport=udp"},
|
||||
URIs: []string{"turn:1.2.3.4:3478?transport=udp", "turns:5.6.7.8:5349"},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(creds)
|
||||
|
||||
@ -10,9 +10,14 @@ type Config struct {
|
||||
// ListenAddr is the address to bind the TURN listener (e.g., "0.0.0.0:3478")
|
||||
ListenAddr string `yaml:"listen_addr"`
|
||||
|
||||
// TLSListenAddr is the address for TURN over TLS/DTLS (e.g., "0.0.0.0:443")
|
||||
// Uses UDP 443 — requires Caddy HTTP/3 (QUIC) to be disabled to avoid port conflict
|
||||
TLSListenAddr string `yaml:"tls_listen_addr"`
|
||||
// TURNSListenAddr is the address for TURNS (TURN over TLS on TCP, e.g., "0.0.0.0:5349")
|
||||
TURNSListenAddr string `yaml:"turns_listen_addr"`
|
||||
|
||||
// TLSCertPath is the path to the TLS certificate PEM file (for TURNS)
|
||||
TLSCertPath string `yaml:"tls_cert_path"`
|
||||
|
||||
// TLSKeyPath is the path to the TLS private key PEM file (for TURNS)
|
||||
TLSKeyPath string `yaml:"tls_key_path"`
|
||||
|
||||
// PublicIP is the public IP address of this node, advertised in TURN allocations
|
||||
PublicIP string `yaml:"public_ip"`
|
||||
|
||||
@ -3,6 +3,7 @@ package turn
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
@ -16,11 +17,12 @@ import (
|
||||
|
||||
// Server wraps a Pion TURN server with namespace-scoped HMAC-SHA1 authentication.
|
||||
type Server struct {
|
||||
config *Config
|
||||
logger *zap.Logger
|
||||
turnServer *pionTurn.Server
|
||||
conn net.PacketConn // UDP listener on primary port (3478)
|
||||
tlsConn net.PacketConn // UDP listener on TLS port (443)
|
||||
config *Config
|
||||
logger *zap.Logger
|
||||
turnServer *pionTurn.Server
|
||||
conn net.PacketConn // UDP listener on primary port (3478)
|
||||
tcpListener net.Listener // Plain TCP listener on primary port (3478)
|
||||
tlsListener net.Listener // TLS TCP listener for TURNS (port 5349)
|
||||
}
|
||||
|
||||
// NewServer creates and starts a TURN server.
|
||||
@ -58,18 +60,45 @@ func NewServer(cfg *Config, logger *zap.Logger) (*Server, error) {
|
||||
},
|
||||
}
|
||||
|
||||
// Create TLS UDP listener (port 443) if configured
|
||||
// Requires Caddy HTTP/3 (QUIC) to be disabled to avoid UDP 443 conflict
|
||||
if cfg.TLSListenAddr != "" {
|
||||
tlsConn, err := net.ListenPacket("udp4", cfg.TLSListenAddr)
|
||||
// Plain TCP listener on same port as UDP (3478) for TCP TURN fallback
|
||||
var listenerConfigs []pionTurn.ListenerConfig
|
||||
tcpListener, err := net.Listen("tcp", cfg.ListenAddr)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to listen TCP on %s: %w", cfg.ListenAddr, err)
|
||||
}
|
||||
s.tcpListener = tcpListener
|
||||
|
||||
listenerConfigs = append(listenerConfigs, pionTurn.ListenerConfig{
|
||||
Listener: tcpListener,
|
||||
RelayAddressGenerator: &pionTurn.RelayAddressGeneratorPortRange{
|
||||
RelayAddress: relayIP,
|
||||
Address: "0.0.0.0",
|
||||
MinPort: uint16(cfg.RelayPortStart),
|
||||
MaxPort: uint16(cfg.RelayPortEnd),
|
||||
},
|
||||
})
|
||||
|
||||
// TURNS: TLS over TCP listener (port 5349) if configured
|
||||
if cfg.TURNSListenAddr != "" && cfg.TLSCertPath != "" && cfg.TLSKeyPath != "" {
|
||||
cert, err := tls.LoadX509KeyPair(cfg.TLSCertPath, cfg.TLSKeyPath)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to listen on %s: %w", cfg.TLSListenAddr, err)
|
||||
return nil, fmt.Errorf("failed to load TLS cert/key: %w", err)
|
||||
}
|
||||
s.tlsConn = tlsConn
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
tlsListener, err := tls.Listen("tcp", cfg.TURNSListenAddr, tlsConfig)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to listen on %s: %w", cfg.TURNSListenAddr, err)
|
||||
}
|
||||
s.tlsListener = tlsListener
|
||||
|
||||
packetConfigs = append(packetConfigs, pionTurn.PacketConnConfig{
|
||||
PacketConn: tlsConn,
|
||||
listenerConfigs = append(listenerConfigs, pionTurn.ListenerConfig{
|
||||
Listener: tlsListener,
|
||||
RelayAddressGenerator: &pionTurn.RelayAddressGeneratorPortRange{
|
||||
RelayAddress: relayIP,
|
||||
Address: "0.0.0.0",
|
||||
@ -80,13 +109,17 @@ func NewServer(cfg *Config, logger *zap.Logger) (*Server, error) {
|
||||
}
|
||||
|
||||
// Create TURN server with HMAC-SHA1 auth
|
||||
turnServer, err := pionTurn.NewServer(pionTurn.ServerConfig{
|
||||
serverConfig := pionTurn.ServerConfig{
|
||||
Realm: cfg.Realm,
|
||||
AuthHandler: func(username, realm string, srcAddr net.Addr) ([]byte, bool) {
|
||||
return s.authHandler(username, realm, srcAddr)
|
||||
},
|
||||
PacketConnConfigs: packetConfigs,
|
||||
})
|
||||
}
|
||||
if len(listenerConfigs) > 0 {
|
||||
serverConfig.ListenerConfigs = listenerConfigs
|
||||
}
|
||||
turnServer, err := pionTurn.NewServer(serverConfig)
|
||||
if err != nil {
|
||||
s.closeListeners()
|
||||
return nil, fmt.Errorf("failed to create TURN server: %w", err)
|
||||
@ -94,8 +127,9 @@ func NewServer(cfg *Config, logger *zap.Logger) (*Server, error) {
|
||||
s.turnServer = turnServer
|
||||
|
||||
s.logger.Info("TURN server started",
|
||||
zap.String("listen_addr", cfg.ListenAddr),
|
||||
zap.String("tls_listen_addr", cfg.TLSListenAddr),
|
||||
zap.String("listen_addr_udp", cfg.ListenAddr),
|
||||
zap.String("listen_addr_tcp", cfg.ListenAddr),
|
||||
zap.String("turns_listen_addr", cfg.TURNSListenAddr),
|
||||
zap.String("public_ip", cfg.PublicIP),
|
||||
zap.String("realm", cfg.Realm),
|
||||
zap.Int("relay_port_start", cfg.RelayPortStart),
|
||||
@ -178,9 +212,13 @@ func (s *Server) closeListeners() {
|
||||
s.conn.Close()
|
||||
s.conn = nil
|
||||
}
|
||||
if s.tlsConn != nil {
|
||||
s.tlsConn.Close()
|
||||
s.tlsConn = nil
|
||||
if s.tcpListener != nil {
|
||||
s.tcpListener.Close()
|
||||
s.tcpListener = nil
|
||||
}
|
||||
if s.tlsListener != nil {
|
||||
s.tlsListener.Close()
|
||||
s.tlsListener = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
83
pkg/turn/tls.go
Normal file
83
pkg/turn/tls.go
Normal file
@ -0,0 +1,83 @@
|
||||
package turn
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateSelfSignedCert generates a self-signed TLS certificate for TURNS.
|
||||
// The certificate is valid for 1 year and includes the public IP as a SAN.
|
||||
func GenerateSelfSignedCert(certPath, keyPath, publicIP string) error {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate private key: %w", err)
|
||||
}
|
||||
|
||||
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate serial number: %w", err)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Orama Network"},
|
||||
CommonName: "TURN Server",
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(publicIP); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create certificate: %w", err)
|
||||
}
|
||||
|
||||
certFile, err := os.Create(certPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create cert file: %w", err)
|
||||
}
|
||||
defer certFile.Close()
|
||||
|
||||
if err := pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil {
|
||||
return fmt.Errorf("failed to write cert PEM: %w", err)
|
||||
}
|
||||
|
||||
keyDER, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal private key: %w", err)
|
||||
}
|
||||
|
||||
keyFile, err := os.Create(keyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create key file: %w", err)
|
||||
}
|
||||
defer keyFile.Close()
|
||||
|
||||
if err := pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}); err != nil {
|
||||
return fmt.Errorf("failed to write key PEM: %w", err)
|
||||
}
|
||||
|
||||
// Restrict key file permissions
|
||||
if err := os.Chmod(keyPath, 0600); err != nil {
|
||||
return fmt.Errorf("failed to set key file permissions: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user