mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 08:36:57 +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
|
.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)
|
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)'
|
LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)'
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
@ -35,7 +37,7 @@ func main() {
|
|||||||
|
|
||||||
// Start HTTP server in background
|
// Start HTTP server in background
|
||||||
go func() {
|
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))
|
logger.ComponentError(logging.ComponentSFU, "SFU server error", zap.Error(err))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,14 +40,16 @@ func parseTURNConfig(logger *logging.ColoredLogger) *turn.Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type yamlCfg struct {
|
type yamlCfg struct {
|
||||||
ListenAddr string `yaml:"listen_addr"`
|
ListenAddr string `yaml:"listen_addr"`
|
||||||
TLSListenAddr string `yaml:"tls_listen_addr"`
|
TURNSListenAddr string `yaml:"turns_listen_addr"`
|
||||||
PublicIP string `yaml:"public_ip"`
|
PublicIP string `yaml:"public_ip"`
|
||||||
Realm string `yaml:"realm"`
|
Realm string `yaml:"realm"`
|
||||||
AuthSecret string `yaml:"auth_secret"`
|
AuthSecret string `yaml:"auth_secret"`
|
||||||
RelayPortStart int `yaml:"relay_port_start"`
|
RelayPortStart int `yaml:"relay_port_start"`
|
||||||
RelayPortEnd int `yaml:"relay_port_end"`
|
RelayPortEnd int `yaml:"relay_port_end"`
|
||||||
Namespace string `yaml:"namespace"`
|
Namespace string `yaml:"namespace"`
|
||||||
|
TLSCertPath string `yaml:"tls_cert_path"`
|
||||||
|
TLSKeyPath string `yaml:"tls_key_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(configPath)
|
data, err := os.ReadFile(configPath)
|
||||||
@ -66,14 +68,16 @@ func parseTURNConfig(logger *logging.ColoredLogger) *turn.Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := &turn.Config{
|
cfg := &turn.Config{
|
||||||
ListenAddr: y.ListenAddr,
|
ListenAddr: y.ListenAddr,
|
||||||
TLSListenAddr: y.TLSListenAddr,
|
TURNSListenAddr: y.TURNSListenAddr,
|
||||||
PublicIP: y.PublicIP,
|
PublicIP: y.PublicIP,
|
||||||
Realm: y.Realm,
|
Realm: y.Realm,
|
||||||
AuthSecret: y.AuthSecret,
|
AuthSecret: y.AuthSecret,
|
||||||
RelayPortStart: y.RelayPortStart,
|
RelayPortStart: y.RelayPortStart,
|
||||||
RelayPortEnd: y.RelayPortEnd,
|
RelayPortEnd: y.RelayPortEnd,
|
||||||
Namespace: y.Namespace,
|
Namespace: y.Namespace,
|
||||||
|
TLSCertPath: y.TLSCertPath,
|
||||||
|
TLSKeyPath: y.TLSKeyPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
if errs := cfg.Validate(); len(errs) > 0 {
|
if errs := cfg.Validate(); len(errs) > 0 {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ type FirewallConfig struct {
|
|||||||
IsNameserver bool // enables port 53 TCP+UDP
|
IsNameserver bool // enables port 53 TCP+UDP
|
||||||
AnyoneORPort int // 0 = disabled, typically 9001
|
AnyoneORPort int // 0 = disabled, typically 9001
|
||||||
WireGuardPort int // default 51820
|
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)
|
TURNRelayStart int // start of TURN relay port range (default 49152)
|
||||||
TURNRelayEnd int // end of TURN relay port range (default 65535)
|
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)
|
// TURN relay (only for nodes running TURN servers)
|
||||||
if fp.config.TURNEnabled {
|
if fp.config.TURNEnabled {
|
||||||
rules = append(rules, "ufw allow 3478/udp") // TURN standard port
|
rules = append(rules, "ufw allow 3478/udp") // TURN standard port (UDP)
|
||||||
rules = append(rules, "ufw allow 443/udp") // TURN TLS port (does not conflict with Caddy TCP 443)
|
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 {
|
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))
|
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 {
|
func (fp *FirewallProvisioner) AddWebRTCRules(relayStart, relayEnd int) error {
|
||||||
rules := []string{
|
rules := []string{
|
||||||
"ufw allow 3478/udp",
|
"ufw allow 3478/udp",
|
||||||
"ufw allow 443/udp",
|
"ufw allow 3478/tcp",
|
||||||
|
"ufw allow 5349/tcp",
|
||||||
}
|
}
|
||||||
if relayStart > 0 && relayEnd > 0 {
|
if relayStart > 0 && relayEnd > 0 {
|
||||||
rules = append(rules, fmt.Sprintf("ufw allow %d:%d/udp", relayStart, relayEnd))
|
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 {
|
func (fp *FirewallProvisioner) RemoveWebRTCRules(relayStart, relayEnd int) error {
|
||||||
rules := []string{
|
rules := []string{
|
||||||
"ufw delete allow 3478/udp",
|
"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 {
|
if relayStart > 0 && relayEnd > 0 {
|
||||||
rules = append(rules, fmt.Sprintf("ufw delete allow %d:%d/udp", relayStart, relayEnd))
|
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")
|
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) {
|
func TestFirewallProvisioner_DefaultPorts(t *testing.T) {
|
||||||
fp := NewFirewallProvisioner(FirewallConfig{})
|
fp := NewFirewallProvisioner(FirewallConfig{})
|
||||||
|
|
||||||
|
|||||||
@ -65,7 +65,7 @@ type SpawnRequest struct {
|
|||||||
|
|
||||||
// TURN config (when action = "spawn-turn")
|
// TURN config (when action = "spawn-turn")
|
||||||
TURNListenAddr string `json:"turn_listen_addr,omitempty"`
|
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"`
|
TURNPublicIP string `json:"turn_public_ip,omitempty"`
|
||||||
TURNRealm string `json:"turn_realm,omitempty"`
|
TURNRealm string `json:"turn_realm,omitempty"`
|
||||||
TURNAuthSecret string `json:"turn_auth_secret,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,
|
Namespace: req.Namespace,
|
||||||
NodeID: req.NodeID,
|
NodeID: req.NodeID,
|
||||||
ListenAddr: req.TURNListenAddr,
|
ListenAddr: req.TURNListenAddr,
|
||||||
TLSListenAddr: req.TURNTLSAddr,
|
TURNSListenAddr: req.TURNTURNSAddr,
|
||||||
PublicIP: req.TURNPublicIP,
|
PublicIP: req.TURNPublicIP,
|
||||||
Realm: req.TURNRealm,
|
Realm: req.TURNRealm,
|
||||||
AuthSecret: req.TURNAuthSecret,
|
AuthSecret: req.TURNAuthSecret,
|
||||||
|
|||||||
@ -38,7 +38,8 @@ func (h *WebRTCHandlers) CredentialsHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
if h.turnDomain != "" {
|
if h.turnDomain != "" {
|
||||||
uris = append(uris,
|
uris = append(uris,
|
||||||
fmt.Sprintf("turn:%s:3478?transport=udp", h.turnDomain),
|
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"])
|
t.Errorf("ttl = %v, want 600", result["ttl"])
|
||||||
}
|
}
|
||||||
uris, ok := result["uris"].([]interface{})
|
uris, ok := result["uris"].([]interface{})
|
||||||
if !ok || len(uris) != 2 {
|
if !ok || len(uris) != 3 {
|
||||||
t.Errorf("uris count = %v, want 2", result["uris"])
|
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)
|
webrtcCfg, err := cm.GetWebRTCConfig(ctx, state.NamespaceName)
|
||||||
if err == nil && webrtcCfg != nil {
|
if err == nil && webrtcCfg != nil {
|
||||||
turnCfg := TURNInstanceConfig{
|
turnCfg := TURNInstanceConfig{
|
||||||
Namespace: state.NamespaceName,
|
Namespace: state.NamespaceName,
|
||||||
NodeID: cm.localNodeID,
|
NodeID: cm.localNodeID,
|
||||||
ListenAddr: fmt.Sprintf("0.0.0.0:%d", state.TURNListenPort),
|
ListenAddr: fmt.Sprintf("0.0.0.0:%d", state.TURNListenPort),
|
||||||
TLSListenAddr: fmt.Sprintf("0.0.0.0:%d", state.TURNTLSPort),
|
TURNSListenAddr: fmt.Sprintf("0.0.0.0:%d", state.TURNTLSPort),
|
||||||
PublicIP: "", // Will be resolved by spawner or from node info
|
PublicIP: "", // Will be resolved by spawner or from node info
|
||||||
Realm: cm.baseDomain,
|
Realm: cm.baseDomain,
|
||||||
AuthSecret: webrtcCfg.TURNSharedSecret,
|
AuthSecret: webrtcCfg.TURNSharedSecret,
|
||||||
RelayPortStart: state.TURNRelayPortStart,
|
RelayPortStart: state.TURNRelayPortStart,
|
||||||
RelayPortEnd: state.TURNRelayPortEnd,
|
RelayPortEnd: state.TURNRelayPortEnd,
|
||||||
}
|
}
|
||||||
if err := cm.systemdSpawner.SpawnTURN(ctx, state.NamespaceName, cm.localNodeID, turnCfg); err != nil {
|
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))
|
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,
|
MediaPortStart: state.SFUMediaPortStart,
|
||||||
MediaPortEnd: state.SFUMediaPortEnd,
|
MediaPortEnd: state.SFUMediaPortEnd,
|
||||||
TURNServers: []sfu.TURNServerConfig{
|
TURNServers: []sfu.TURNServerConfig{
|
||||||
{Host: turnDomain, Port: TURNDefaultPort},
|
{Host: turnDomain, Port: TURNDefaultPort, Secure: false},
|
||||||
{Host: turnDomain, Port: TURNTLSPort},
|
{Host: turnDomain, Port: TURNSPort, Secure: true},
|
||||||
},
|
},
|
||||||
TURNSecret: webrtcCfg.TURNSharedSecret,
|
TURNSecret: webrtcCfg.TURNSharedSecret,
|
||||||
TURNCredTTL: webrtcCfg.TURNCredentialTTL,
|
TURNCredTTL: webrtcCfg.TURNCredentialTTL,
|
||||||
|
|||||||
@ -102,8 +102,8 @@ func (cm *ClusterManager) EnableWebRTC(ctx context.Context, namespaceName, enabl
|
|||||||
// 9. Build TURN server list for SFU config
|
// 9. Build TURN server list for SFU config
|
||||||
turnDomain := fmt.Sprintf("turn.ns-%s.%s", namespaceName, cm.baseDomain)
|
turnDomain := fmt.Sprintf("turn.ns-%s.%s", namespaceName, cm.baseDomain)
|
||||||
turnServers := []sfu.TURNServerConfig{
|
turnServers := []sfu.TURNServerConfig{
|
||||||
{Host: turnDomain, Port: TURNDefaultPort},
|
{Host: turnDomain, Port: TURNDefaultPort, Secure: false},
|
||||||
{Host: turnDomain, Port: TURNTLSPort},
|
{Host: turnDomain, Port: TURNSPort, Secure: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. Get port blocks for RQLite DSN
|
// 10. Get port blocks for RQLite DSN
|
||||||
@ -123,15 +123,15 @@ func (cm *ClusterManager) EnableWebRTC(ctx context.Context, namespaceName, enabl
|
|||||||
for _, node := range turnNodes {
|
for _, node := range turnNodes {
|
||||||
turnBlock := turnBlocks[node.NodeID]
|
turnBlock := turnBlocks[node.NodeID]
|
||||||
turnCfg := TURNInstanceConfig{
|
turnCfg := TURNInstanceConfig{
|
||||||
Namespace: namespaceName,
|
Namespace: namespaceName,
|
||||||
NodeID: node.NodeID,
|
NodeID: node.NodeID,
|
||||||
ListenAddr: fmt.Sprintf("0.0.0.0:%d", turnBlock.TURNListenPort),
|
ListenAddr: fmt.Sprintf("0.0.0.0:%d", turnBlock.TURNListenPort),
|
||||||
TLSListenAddr: fmt.Sprintf("0.0.0.0:%d", turnBlock.TURNTLSPort),
|
TURNSListenAddr: fmt.Sprintf("0.0.0.0:%d", turnBlock.TURNTLSPort),
|
||||||
PublicIP: node.PublicIP,
|
PublicIP: node.PublicIP,
|
||||||
Realm: cm.baseDomain,
|
Realm: cm.baseDomain,
|
||||||
AuthSecret: turnSecret,
|
AuthSecret: turnSecret,
|
||||||
RelayPortStart: turnBlock.TURNRelayPortStart,
|
RelayPortStart: turnBlock.TURNRelayPortStart,
|
||||||
RelayPortEnd: turnBlock.TURNRelayPortEnd,
|
RelayPortEnd: turnBlock.TURNRelayPortEnd,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cm.spawnTURNOnNode(ctx, node, namespaceName, turnCfg); err != nil {
|
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)
|
turnIPs = append(turnIPs, node.PublicIP)
|
||||||
}
|
}
|
||||||
if err := cm.dnsManager.CreateTURNRecords(ctx, namespaceName, turnIPs); err != nil {
|
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.String("namespace", namespaceName),
|
||||||
zap.Error(err))
|
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
|
// 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))
|
turnServers := make([]map[string]interface{}, len(cfg.TURNServers))
|
||||||
for i, ts := range cfg.TURNServers {
|
for i, ts := range cfg.TURNServers {
|
||||||
turnServers[i] = map[string]interface{}{
|
turnServers[i] = map[string]interface{}{
|
||||||
"host": ts.Host,
|
"host": ts.Host,
|
||||||
"port": ts.Port,
|
"port": ts.Port,
|
||||||
|
"secure": ts.Secure,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -465,7 +468,7 @@ func (cm *ClusterManager) spawnTURNRemote(ctx context.Context, nodeIP string, cf
|
|||||||
"namespace": cfg.Namespace,
|
"namespace": cfg.Namespace,
|
||||||
"node_id": cfg.NodeID,
|
"node_id": cfg.NodeID,
|
||||||
"turn_listen_addr": cfg.ListenAddr,
|
"turn_listen_addr": cfg.ListenAddr,
|
||||||
"turn_tls_addr": cfg.TLSListenAddr,
|
"turn_turns_addr": cfg.TURNSListenAddr,
|
||||||
"turn_public_ip": cfg.PublicIP,
|
"turn_public_ip": cfg.PublicIP,
|
||||||
"turn_realm": cfg.Realm,
|
"turn_realm": cfg.Realm,
|
||||||
"turn_auth_secret": cfg.AuthSecret,
|
"turn_auth_secret": cfg.AuthSecret,
|
||||||
|
|||||||
@ -61,7 +61,7 @@ func (drm *DNSRecordManager) CreateNamespaceRecords(ctx context.Context, namespa
|
|||||||
insertQuery := `
|
insertQuery := `
|
||||||
INSERT INTO dns_records (
|
INSERT INTO dns_records (
|
||||||
id, fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at
|
id, fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, TRUE, ?, ?)
|
||||||
`
|
`
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
_, err := drm.db.Exec(internalCtx, insertQuery,
|
_, 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
|
60, // 60 second TTL for quick failover
|
||||||
"namespace:"+namespaceName, // Track ownership with namespace prefix
|
"namespace:"+namespaceName, // Track ownership with namespace prefix
|
||||||
"cluster-manager", // Created by the cluster manager
|
"cluster-manager", // Created by the cluster manager
|
||||||
true, // Active
|
|
||||||
now,
|
now,
|
||||||
now,
|
now,
|
||||||
)
|
)
|
||||||
@ -96,7 +95,7 @@ func (drm *DNSRecordManager) CreateNamespaceRecords(ctx context.Context, namespa
|
|||||||
insertQuery := `
|
insertQuery := `
|
||||||
INSERT INTO dns_records (
|
INSERT INTO dns_records (
|
||||||
id, fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at
|
id, fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, TRUE, ?, ?)
|
||||||
`
|
`
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
_, err := drm.db.Exec(internalCtx, insertQuery,
|
_, err := drm.db.Exec(internalCtx, insertQuery,
|
||||||
@ -107,7 +106,6 @@ func (drm *DNSRecordManager) CreateNamespaceRecords(ctx context.Context, namespa
|
|||||||
60,
|
60,
|
||||||
"namespace:"+namespaceName,
|
"namespace:"+namespaceName,
|
||||||
"cluster-manager",
|
"cluster-manager",
|
||||||
true,
|
|
||||||
now,
|
now,
|
||||||
now,
|
now,
|
||||||
)
|
)
|
||||||
@ -230,11 +228,11 @@ func (drm *DNSRecordManager) AddNamespaceRecord(ctx context.Context, namespaceNa
|
|||||||
insertQuery := `
|
insertQuery := `
|
||||||
INSERT INTO dns_records (
|
INSERT INTO dns_records (
|
||||||
id, fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at
|
id, fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, TRUE, ?, ?)
|
||||||
`
|
`
|
||||||
_, err := drm.db.Exec(internalCtx, insertQuery,
|
_, err := drm.db.Exec(internalCtx, insertQuery,
|
||||||
recordID, f, "A", ip, 60,
|
recordID, f, "A", ip, 60,
|
||||||
"namespace:"+namespaceName, "cluster-manager", true, now, now,
|
"namespace:"+namespaceName, "cluster-manager", now, now,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ClusterError{
|
return &ClusterError{
|
||||||
@ -328,13 +326,13 @@ func (drm *DNSRecordManager) CreateTURNRecords(ctx context.Context, namespaceNam
|
|||||||
insertQuery := `
|
insertQuery := `
|
||||||
INSERT INTO dns_records (
|
INSERT INTO dns_records (
|
||||||
id, fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at
|
id, fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, TRUE, ?, ?)
|
||||||
`
|
`
|
||||||
_, err := drm.db.Exec(internalCtx, insertQuery,
|
_, err := drm.db.Exec(internalCtx, insertQuery,
|
||||||
recordID, fqdn, "A", ip, 60,
|
recordID, fqdn, "A", ip, 60,
|
||||||
"namespace-turn:"+namespaceName,
|
"namespace-turn:"+namespaceName,
|
||||||
"cluster-manager",
|
"cluster-manager",
|
||||||
true, now, now,
|
now, now,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ClusterError{
|
return &ClusterError{
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
production "github.com/DeBrosOfficial/network/pkg/environments/production"
|
||||||
"github.com/DeBrosOfficial/network/pkg/gateway"
|
"github.com/DeBrosOfficial/network/pkg/gateway"
|
||||||
"github.com/DeBrosOfficial/network/pkg/olric"
|
"github.com/DeBrosOfficial/network/pkg/olric"
|
||||||
"github.com/DeBrosOfficial/network/pkg/rqlite"
|
"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
|
// TURNInstanceConfig holds configuration for spawning a TURN instance
|
||||||
type TURNInstanceConfig struct {
|
type TURNInstanceConfig struct {
|
||||||
Namespace string
|
Namespace string
|
||||||
NodeID string
|
NodeID string
|
||||||
ListenAddr string // e.g., "0.0.0.0:3478"
|
ListenAddr string // e.g., "0.0.0.0:3478"
|
||||||
TLSListenAddr string // e.g., "0.0.0.0:443" (UDP, no conflict with Caddy TCP)
|
TURNSListenAddr string // e.g., "0.0.0.0:5349" (TURNS over TLS/TCP)
|
||||||
PublicIP string // Public IP for TURN relay allocations
|
PublicIP string // Public IP for TURN relay allocations
|
||||||
Realm string // TURN realm (typically base domain)
|
Realm string // TURN realm (typically base domain)
|
||||||
AuthSecret string // HMAC-SHA1 shared secret
|
AuthSecret string // HMAC-SHA1 shared secret
|
||||||
RelayPortStart int // Start of relay port range
|
RelayPortStart int // Start of relay port range
|
||||||
RelayPortEnd int // End of relay port range
|
RelayPortEnd int // End of relay port range
|
||||||
}
|
}
|
||||||
|
|
||||||
// SpawnTURN starts a TURN instance using systemd
|
// 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))
|
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
|
// Build TURN YAML config
|
||||||
turnConfig := turn.Config{
|
turnConfig := turn.Config{
|
||||||
ListenAddr: cfg.ListenAddr,
|
ListenAddr: cfg.ListenAddr,
|
||||||
TLSListenAddr: cfg.TLSListenAddr,
|
TURNSListenAddr: cfg.TURNSListenAddr,
|
||||||
PublicIP: cfg.PublicIP,
|
PublicIP: cfg.PublicIP,
|
||||||
Realm: cfg.Realm,
|
Realm: cfg.Realm,
|
||||||
AuthSecret: cfg.AuthSecret,
|
AuthSecret: cfg.AuthSecret,
|
||||||
RelayPortStart: cfg.RelayPortStart,
|
RelayPortStart: cfg.RelayPortStart,
|
||||||
RelayPortEnd: cfg.RelayPortEnd,
|
RelayPortEnd: cfg.RelayPortEnd,
|
||||||
Namespace: cfg.Namespace,
|
Namespace: cfg.Namespace,
|
||||||
|
}
|
||||||
|
if cfg.TURNSListenAddr != "" {
|
||||||
|
turnConfig.TLSCertPath = certPath
|
||||||
|
turnConfig.TLSKeyPath = keyPath
|
||||||
}
|
}
|
||||||
|
|
||||||
configBytes, err := yaml.Marshal(turnConfig)
|
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)
|
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",
|
s.logger.Info("TURN spawned successfully via systemd",
|
||||||
zap.String("namespace", namespace),
|
zap.String("namespace", namespace),
|
||||||
zap.String("node_id", nodeID))
|
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("namespace", namespace),
|
||||||
zap.String("node_id", nodeID))
|
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.
|
// SaveClusterState writes cluster state JSON to the namespace data directory.
|
||||||
|
|||||||
@ -110,8 +110,8 @@ const (
|
|||||||
TURNRelayPortsPerNamespace = 800
|
TURNRelayPortsPerNamespace = 800
|
||||||
|
|
||||||
// TURN listen ports (standard)
|
// TURN listen ports (standard)
|
||||||
TURNDefaultPort = 3478
|
TURNDefaultPort = 3478
|
||||||
TURNTLSPort = 443
|
TURNSPort = 5349 // TURNS (TURN over TLS on TCP)
|
||||||
|
|
||||||
// Default TURN credential TTL in seconds (10 minutes)
|
// Default TURN credential TTL in seconds (10 minutes)
|
||||||
DefaultTURNCredentialTTL = 600
|
DefaultTURNCredentialTTL = 600
|
||||||
|
|||||||
@ -217,7 +217,7 @@ func (wpa *WebRTCPortAllocator) tryAllocateTURNPorts(ctx context.Context, nodeID
|
|||||||
NamespaceClusterID: namespaceClusterID,
|
NamespaceClusterID: namespaceClusterID,
|
||||||
ServiceType: "turn",
|
ServiceType: "turn",
|
||||||
TURNListenPort: TURNDefaultPort,
|
TURNListenPort: TURNDefaultPort,
|
||||||
TURNTLSPort: TURNTLSPort,
|
TURNTLSPort: TURNSPort,
|
||||||
TURNRelayPortStart: relayStart,
|
TURNRelayPortStart: relayStart,
|
||||||
TURNRelayPortEnd: relayStart + TURNRelayPortsPerNamespace - 1,
|
TURNRelayPortEnd: relayStart + TURNRelayPortsPerNamespace - 1,
|
||||||
AllocatedAt: time.Now(),
|
AllocatedAt: time.Now(),
|
||||||
|
|||||||
@ -168,8 +168,8 @@ func TestWebRTCPortAllocator_AllocateTURNPorts(t *testing.T) {
|
|||||||
if block.TURNListenPort != TURNDefaultPort {
|
if block.TURNListenPort != TURNDefaultPort {
|
||||||
t.Errorf("TURNListenPort = %d, want %d", block.TURNListenPort, TURNDefaultPort)
|
t.Errorf("TURNListenPort = %d, want %d", block.TURNListenPort, TURNDefaultPort)
|
||||||
}
|
}
|
||||||
if block.TURNTLSPort != TURNTLSPort {
|
if block.TURNTLSPort != TURNSPort {
|
||||||
t.Errorf("TURNTLSPort = %d, want %d", block.TURNTLSPort, TURNTLSPort)
|
t.Errorf("TURNTLSPort = %d, want %d", block.TURNTLSPort, TURNSPort)
|
||||||
}
|
}
|
||||||
if block.TURNRelayPortStart != TURNRelayPortRangeStart {
|
if block.TURNRelayPortStart != TURNRelayPortRangeStart {
|
||||||
t.Errorf("TURNRelayPortStart = %d, want %d", 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",
|
NamespaceClusterID: "cluster-1",
|
||||||
ServiceType: "turn",
|
ServiceType: "turn",
|
||||||
TURNListenPort: 3478,
|
TURNListenPort: 3478,
|
||||||
TURNTLSPort: 443,
|
TURNTLSPort: 5349,
|
||||||
TURNRelayPortStart: 49152,
|
TURNRelayPortStart: 49152,
|
||||||
TURNRelayPortEnd: 49951,
|
TURNRelayPortEnd: 49951,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,8 +30,9 @@ type Config struct {
|
|||||||
|
|
||||||
// TURNServerConfig represents a single TURN server endpoint
|
// TURNServerConfig represents a single TURN server endpoint
|
||||||
type TURNServerConfig struct {
|
type TURNServerConfig struct {
|
||||||
Host string `yaml:"host"` // IP or hostname
|
Host string `yaml:"host"` // IP or hostname
|
||||||
Port int `yaml:"port"` // UDP port (3478 or 443)
|
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
|
// Validate checks the SFU configuration for errors
|
||||||
|
|||||||
@ -539,7 +539,12 @@ func (r *Room) buildICEServers() []webrtc.ICEServer {
|
|||||||
|
|
||||||
var urls []string
|
var urls []string
|
||||||
for _, ts := range r.config.TURNServers {
|
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
|
ttl := time.Duration(r.config.TURNCredentialTTL) * time.Second
|
||||||
|
|||||||
@ -179,11 +179,14 @@ func TestRoomBuildICEServers(t *testing.T) {
|
|||||||
if len(servers) != 1 {
|
if len(servers) != 1 {
|
||||||
t.Fatalf("ICE servers count = %d, want 1", len(servers))
|
t.Fatalf("ICE servers count = %d, want 1", len(servers))
|
||||||
}
|
}
|
||||||
if len(servers[0].URLs) != 1 {
|
if len(servers[0].URLs) != 2 {
|
||||||
t.Fatalf("URLs count = %d, want 1", len(servers[0].URLs))
|
t.Fatalf("URLs count = %d, want 2", len(servers[0].URLs))
|
||||||
}
|
}
|
||||||
if servers[0].URLs[0] != "turn:1.2.3.4:3478?transport=udp" {
|
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 == "" {
|
if servers[0].Username == "" {
|
||||||
t.Error("Username should not be empty")
|
t.Error("Username should not be empty")
|
||||||
@ -222,8 +225,8 @@ func TestRoomBuildICEServersNoSecret(t *testing.T) {
|
|||||||
func TestRoomBuildICEServersMultipleTURN(t *testing.T) {
|
func TestRoomBuildICEServersMultipleTURN(t *testing.T) {
|
||||||
cfg := testConfig()
|
cfg := testConfig()
|
||||||
cfg.TURNServers = []TURNServerConfig{
|
cfg.TURNServers = []TURNServerConfig{
|
||||||
{Host: "1.2.3.4", Port: 3478},
|
{Host: "1.2.3.4", Port: 3478}, // non-secure → UDP + TCP = 2 URIs
|
||||||
{Host: "5.6.7.8", Port: 443},
|
{Host: "5.6.7.8", Port: 5349, Secure: true}, // secure → 1 URI
|
||||||
}
|
}
|
||||||
|
|
||||||
rm := NewRoomManager(cfg, testLogger())
|
rm := NewRoomManager(cfg, testLogger())
|
||||||
@ -233,8 +236,9 @@ func TestRoomBuildICEServersMultipleTURN(t *testing.T) {
|
|||||||
if len(servers) != 1 {
|
if len(servers) != 1 {
|
||||||
t.Fatalf("ICE servers count = %d, want 1", len(servers))
|
t.Fatalf("ICE servers count = %d, want 1", len(servers))
|
||||||
}
|
}
|
||||||
if len(servers[0].URLs) != 2 {
|
// 1 non-secure (UDP+TCP) + 1 secure (TURNS) = 3 URIs
|
||||||
t.Fatalf("URLs count = %d, want 2", len(servers[0].URLs))
|
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
|
var uris []string
|
||||||
for _, ts := range s.config.TURNServers {
|
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{
|
peer.SendMessage(NewServerMessage(MessageTypeTURNCredentials, &TURNCredentialsData{
|
||||||
|
|||||||
@ -233,7 +233,7 @@ func TestTURNCredentialsDataSerialization(t *testing.T) {
|
|||||||
Username: "1234567890:test-ns",
|
Username: "1234567890:test-ns",
|
||||||
Password: "base64password==",
|
Password: "base64password==",
|
||||||
TTL: 600,
|
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)
|
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 is the address to bind the TURN listener (e.g., "0.0.0.0:3478")
|
||||||
ListenAddr string `yaml:"listen_addr"`
|
ListenAddr string `yaml:"listen_addr"`
|
||||||
|
|
||||||
// TLSListenAddr is the address for TURN over TLS/DTLS (e.g., "0.0.0.0:443")
|
// TURNSListenAddr is the address for TURNS (TURN over TLS on TCP, e.g., "0.0.0.0:5349")
|
||||||
// Uses UDP 443 — requires Caddy HTTP/3 (QUIC) to be disabled to avoid port conflict
|
TURNSListenAddr string `yaml:"turns_listen_addr"`
|
||||||
TLSListenAddr string `yaml:"tls_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 is the public IP address of this node, advertised in TURN allocations
|
||||||
PublicIP string `yaml:"public_ip"`
|
PublicIP string `yaml:"public_ip"`
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package turn
|
|||||||
import (
|
import (
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@ -16,11 +17,12 @@ import (
|
|||||||
|
|
||||||
// Server wraps a Pion TURN server with namespace-scoped HMAC-SHA1 authentication.
|
// Server wraps a Pion TURN server with namespace-scoped HMAC-SHA1 authentication.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
config *Config
|
config *Config
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
turnServer *pionTurn.Server
|
turnServer *pionTurn.Server
|
||||||
conn net.PacketConn // UDP listener on primary port (3478)
|
conn net.PacketConn // UDP listener on primary port (3478)
|
||||||
tlsConn net.PacketConn // UDP listener on TLS port (443)
|
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.
|
// 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
|
// Plain TCP listener on same port as UDP (3478) for TCP TURN fallback
|
||||||
// Requires Caddy HTTP/3 (QUIC) to be disabled to avoid UDP 443 conflict
|
var listenerConfigs []pionTurn.ListenerConfig
|
||||||
if cfg.TLSListenAddr != "" {
|
tcpListener, err := net.Listen("tcp", cfg.ListenAddr)
|
||||||
tlsConn, err := net.ListenPacket("udp4", cfg.TLSListenAddr)
|
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 {
|
if err != nil {
|
||||||
conn.Close()
|
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{
|
listenerConfigs = append(listenerConfigs, pionTurn.ListenerConfig{
|
||||||
PacketConn: tlsConn,
|
Listener: tlsListener,
|
||||||
RelayAddressGenerator: &pionTurn.RelayAddressGeneratorPortRange{
|
RelayAddressGenerator: &pionTurn.RelayAddressGeneratorPortRange{
|
||||||
RelayAddress: relayIP,
|
RelayAddress: relayIP,
|
||||||
Address: "0.0.0.0",
|
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
|
// Create TURN server with HMAC-SHA1 auth
|
||||||
turnServer, err := pionTurn.NewServer(pionTurn.ServerConfig{
|
serverConfig := pionTurn.ServerConfig{
|
||||||
Realm: cfg.Realm,
|
Realm: cfg.Realm,
|
||||||
AuthHandler: func(username, realm string, srcAddr net.Addr) ([]byte, bool) {
|
AuthHandler: func(username, realm string, srcAddr net.Addr) ([]byte, bool) {
|
||||||
return s.authHandler(username, realm, srcAddr)
|
return s.authHandler(username, realm, srcAddr)
|
||||||
},
|
},
|
||||||
PacketConnConfigs: packetConfigs,
|
PacketConnConfigs: packetConfigs,
|
||||||
})
|
}
|
||||||
|
if len(listenerConfigs) > 0 {
|
||||||
|
serverConfig.ListenerConfigs = listenerConfigs
|
||||||
|
}
|
||||||
|
turnServer, err := pionTurn.NewServer(serverConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.closeListeners()
|
s.closeListeners()
|
||||||
return nil, fmt.Errorf("failed to create TURN server: %w", err)
|
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.turnServer = turnServer
|
||||||
|
|
||||||
s.logger.Info("TURN server started",
|
s.logger.Info("TURN server started",
|
||||||
zap.String("listen_addr", cfg.ListenAddr),
|
zap.String("listen_addr_udp", cfg.ListenAddr),
|
||||||
zap.String("tls_listen_addr", cfg.TLSListenAddr),
|
zap.String("listen_addr_tcp", cfg.ListenAddr),
|
||||||
|
zap.String("turns_listen_addr", cfg.TURNSListenAddr),
|
||||||
zap.String("public_ip", cfg.PublicIP),
|
zap.String("public_ip", cfg.PublicIP),
|
||||||
zap.String("realm", cfg.Realm),
|
zap.String("realm", cfg.Realm),
|
||||||
zap.Int("relay_port_start", cfg.RelayPortStart),
|
zap.Int("relay_port_start", cfg.RelayPortStart),
|
||||||
@ -178,9 +212,13 @@ func (s *Server) closeListeners() {
|
|||||||
s.conn.Close()
|
s.conn.Close()
|
||||||
s.conn = nil
|
s.conn = nil
|
||||||
}
|
}
|
||||||
if s.tlsConn != nil {
|
if s.tcpListener != nil {
|
||||||
s.tlsConn.Close()
|
s.tcpListener.Close()
|
||||||
s.tlsConn = nil
|
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