feat: add WebRTC configuration support for gateway instances

This commit is contained in:
anonpenguin23 2026-02-22 13:11:16 +02:00
parent 552fde428e
commit 3597c61cfc
7 changed files with 299 additions and 34 deletions

1
.gitignore vendored
View File

@ -93,3 +93,4 @@ terms-agreement
./cli ./cli
./inspector ./inspector
docs/later_todos/ docs/later_todos/
sim/

View File

@ -48,6 +48,11 @@ type SpawnRequest struct {
IPFSAPIURL string `json:"ipfs_api_url,omitempty"` IPFSAPIURL string `json:"ipfs_api_url,omitempty"`
IPFSTimeout string `json:"ipfs_timeout,omitempty"` IPFSTimeout string `json:"ipfs_timeout,omitempty"`
IPFSReplicationFactor int `json:"ipfs_replication_factor,omitempty"` IPFSReplicationFactor int `json:"ipfs_replication_factor,omitempty"`
// Gateway WebRTC config (when action = "spawn-gateway" and WebRTC is enabled)
GatewayWebRTCEnabled bool `json:"gateway_webrtc_enabled,omitempty"`
GatewaySFUPort int `json:"gateway_sfu_port,omitempty"`
GatewayTURNDomain string `json:"gateway_turn_domain,omitempty"`
GatewayTURNSecret string `json:"gateway_turn_secret,omitempty"`
// SFU config (when action = "spawn-sfu") // SFU config (when action = "spawn-sfu")
SFUListenAddr string `json:"sfu_listen_addr,omitempty"` SFUListenAddr string `json:"sfu_listen_addr,omitempty"`
@ -225,6 +230,10 @@ func (h *SpawnHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
IPFSAPIURL: req.IPFSAPIURL, IPFSAPIURL: req.IPFSAPIURL,
IPFSTimeout: ipfsTimeout, IPFSTimeout: ipfsTimeout,
IPFSReplicationFactor: req.IPFSReplicationFactor, IPFSReplicationFactor: req.IPFSReplicationFactor,
WebRTCEnabled: req.GatewayWebRTCEnabled,
SFUPort: req.GatewaySFUPort,
TURNDomain: req.GatewayTURNDomain,
TURNSecret: req.GatewayTURNSecret,
} }
if err := h.systemdSpawner.SpawnGateway(ctx, req.Namespace, req.NodeID, cfg); err != nil { if err := h.systemdSpawner.SpawnGateway(ctx, req.Namespace, req.NodeID, cfg); err != nil {
h.logger.Error("Failed to spawn Gateway instance", zap.Error(err)) h.logger.Error("Failed to spawn Gateway instance", zap.Error(err))
@ -241,6 +250,51 @@ func (h *SpawnHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
writeSpawnResponse(w, http.StatusOK, SpawnResponse{Success: true}) writeSpawnResponse(w, http.StatusOK, SpawnResponse{Success: true})
case "restart-gateway":
// Restart gateway with updated config (used by EnableWebRTC/DisableWebRTC)
var ipfsTimeout time.Duration
if req.IPFSTimeout != "" {
var err error
ipfsTimeout, err = time.ParseDuration(req.IPFSTimeout)
if err != nil {
ipfsTimeout = 60 * time.Second
}
}
var olricTimeout time.Duration
if req.GatewayOlricTimeout != "" {
var err error
olricTimeout, err = time.ParseDuration(req.GatewayOlricTimeout)
if err != nil {
olricTimeout = 30 * time.Second
}
} else {
olricTimeout = 30 * time.Second
}
cfg := gateway.InstanceConfig{
Namespace: req.Namespace,
NodeID: req.NodeID,
HTTPPort: req.GatewayHTTPPort,
BaseDomain: req.GatewayBaseDomain,
RQLiteDSN: req.GatewayRQLiteDSN,
GlobalRQLiteDSN: req.GatewayGlobalRQLiteDSN,
OlricServers: req.GatewayOlricServers,
OlricTimeout: olricTimeout,
IPFSClusterAPIURL: req.IPFSClusterAPIURL,
IPFSAPIURL: req.IPFSAPIURL,
IPFSTimeout: ipfsTimeout,
IPFSReplicationFactor: req.IPFSReplicationFactor,
WebRTCEnabled: req.GatewayWebRTCEnabled,
SFUPort: req.GatewaySFUPort,
TURNDomain: req.GatewayTURNDomain,
TURNSecret: req.GatewayTURNSecret,
}
if err := h.systemdSpawner.RestartGateway(ctx, req.Namespace, req.NodeID, cfg); err != nil {
h.logger.Error("Failed to restart Gateway instance", zap.Error(err))
writeSpawnResponse(w, http.StatusInternalServerError, SpawnResponse{Error: err.Error()})
return
}
writeSpawnResponse(w, http.StatusOK, SpawnResponse{Success: true})
case "save-cluster-state": case "save-cluster-state":
if len(req.ClusterState) == 0 { if len(req.ClusterState) == 0 {
writeSpawnResponse(w, http.StatusBadRequest, SpawnResponse{Error: "cluster_state is required"}) writeSpawnResponse(w, http.StatusBadRequest, SpawnResponse{Error: "cluster_state is required"})

View File

@ -90,26 +90,41 @@ type InstanceConfig struct {
IPFSAPIURL string // IPFS API URL (e.g., "http://localhost:5001") IPFSAPIURL string // IPFS API URL (e.g., "http://localhost:5001")
IPFSTimeout time.Duration // Timeout for IPFS operations IPFSTimeout time.Duration // Timeout for IPFS operations
IPFSReplicationFactor int // IPFS replication factor IPFSReplicationFactor int // IPFS replication factor
// WebRTC configuration (populated when WebRTC is enabled for the namespace)
WebRTCEnabled bool // Enable WebRTC (SFU/TURN) routes on this gateway
SFUPort int // SFU signaling port on this node
TURNDomain string // TURN server domain (e.g., "turn.ns-alice.orama-devnet.network")
TURNSecret string // TURN shared secret for credential generation
}
// GatewayYAMLWebRTC represents the webrtc section of the gateway YAML config.
// Must match yamlWebRTCCfg in cmd/gateway/config.go.
type GatewayYAMLWebRTC struct {
Enabled bool `yaml:"enabled"`
SFUPort int `yaml:"sfu_port,omitempty"`
TURNDomain string `yaml:"turn_domain,omitempty"`
TURNSecret string `yaml:"turn_secret,omitempty"`
} }
// GatewayYAMLConfig represents the gateway YAML configuration structure // GatewayYAMLConfig represents the gateway YAML configuration structure
// This must match the yamlCfg struct in cmd/gateway/config.go exactly // This must match the yamlCfg struct in cmd/gateway/config.go exactly
// because the gateway uses strict YAML decoding that rejects unknown fields // because the gateway uses strict YAML decoding that rejects unknown fields
type GatewayYAMLConfig struct { type GatewayYAMLConfig struct {
ListenAddr string `yaml:"listen_addr"` ListenAddr string `yaml:"listen_addr"`
ClientNamespace string `yaml:"client_namespace"` ClientNamespace string `yaml:"client_namespace"`
RQLiteDSN string `yaml:"rqlite_dsn"` RQLiteDSN string `yaml:"rqlite_dsn"`
GlobalRQLiteDSN string `yaml:"global_rqlite_dsn,omitempty"` GlobalRQLiteDSN string `yaml:"global_rqlite_dsn,omitempty"`
BootstrapPeers []string `yaml:"bootstrap_peers,omitempty"` BootstrapPeers []string `yaml:"bootstrap_peers,omitempty"`
EnableHTTPS bool `yaml:"enable_https,omitempty"` EnableHTTPS bool `yaml:"enable_https,omitempty"`
DomainName string `yaml:"domain_name,omitempty"` DomainName string `yaml:"domain_name,omitempty"`
TLSCacheDir string `yaml:"tls_cache_dir,omitempty"` TLSCacheDir string `yaml:"tls_cache_dir,omitempty"`
OlricServers []string `yaml:"olric_servers"` OlricServers []string `yaml:"olric_servers"`
OlricTimeout string `yaml:"olric_timeout,omitempty"` OlricTimeout string `yaml:"olric_timeout,omitempty"`
IPFSClusterAPIURL string `yaml:"ipfs_cluster_api_url,omitempty"` IPFSClusterAPIURL string `yaml:"ipfs_cluster_api_url,omitempty"`
IPFSAPIURL string `yaml:"ipfs_api_url,omitempty"` IPFSAPIURL string `yaml:"ipfs_api_url,omitempty"`
IPFSTimeout string `yaml:"ipfs_timeout,omitempty"` IPFSTimeout string `yaml:"ipfs_timeout,omitempty"`
IPFSReplicationFactor int `yaml:"ipfs_replication_factor,omitempty"` IPFSReplicationFactor int `yaml:"ipfs_replication_factor,omitempty"`
WebRTC GatewayYAMLWebRTC `yaml:"webrtc,omitempty"`
} }
// NewInstanceSpawner creates a new Gateway instance spawner // NewInstanceSpawner creates a new Gateway instance spawner
@ -294,6 +309,12 @@ func (is *InstanceSpawner) generateConfig(configPath string, cfg InstanceConfig,
IPFSClusterAPIURL: cfg.IPFSClusterAPIURL, IPFSClusterAPIURL: cfg.IPFSClusterAPIURL,
IPFSAPIURL: cfg.IPFSAPIURL, IPFSAPIURL: cfg.IPFSAPIURL,
IPFSReplicationFactor: cfg.IPFSReplicationFactor, IPFSReplicationFactor: cfg.IPFSReplicationFactor,
WebRTC: GatewayYAMLWebRTC{
Enabled: cfg.WebRTCEnabled,
SFUPort: cfg.SFUPort,
TURNDomain: cfg.TURNDomain,
TURNSecret: cfg.TURNSecret,
},
} }
// Set Olric timeout if provided // Set Olric timeout if provided
if cfg.OlricTimeout > 0 { if cfg.OlricTimeout > 0 {

View File

@ -661,6 +661,10 @@ func (cm *ClusterManager) spawnGatewayRemote(ctx context.Context, nodeIP string,
"ipfs_api_url": cfg.IPFSAPIURL, "ipfs_api_url": cfg.IPFSAPIURL,
"ipfs_timeout": ipfsTimeout, "ipfs_timeout": ipfsTimeout,
"ipfs_replication_factor": cfg.IPFSReplicationFactor, "ipfs_replication_factor": cfg.IPFSReplicationFactor,
"gateway_webrtc_enabled": cfg.WebRTCEnabled,
"gateway_sfu_port": cfg.SFUPort,
"gateway_turn_domain": cfg.TURNDomain,
"gateway_turn_secret": cfg.TURNSecret,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -1555,6 +1559,16 @@ func (cm *ClusterManager) restoreClusterOnNode(ctx context.Context, clusterID, n
IPFSReplicationFactor: cm.ipfsReplicationFactor, IPFSReplicationFactor: cm.ipfsReplicationFactor,
} }
// Add WebRTC config if enabled for this namespace
if webrtcCfg, err := cm.GetWebRTCConfig(ctx, namespaceName); err == nil && webrtcCfg != nil {
if sfuBlock, err := cm.webrtcPortAllocator.GetSFUPorts(ctx, clusterID, cm.localNodeID); err == nil && sfuBlock != nil {
gwCfg.WebRTCEnabled = true
gwCfg.SFUPort = sfuBlock.SFUSignalingPort
gwCfg.TURNDomain = fmt.Sprintf("turn.ns-%s.%s", namespaceName, cm.baseDomain)
gwCfg.TURNSecret = webrtcCfg.TURNSharedSecret
}
}
if err := cm.spawnGatewayWithSystemd(ctx, gwCfg); err != nil { if err := cm.spawnGatewayWithSystemd(ctx, gwCfg); err != nil {
cm.logger.Error("Failed to restore Gateway", zap.String("namespace", namespaceName), zap.Error(err)) cm.logger.Error("Failed to restore Gateway", zap.String("namespace", namespaceName), zap.Error(err))
} else { } else {
@ -1617,7 +1631,8 @@ type ClusterLocalState struct {
// WebRTC fields (zero values when WebRTC not enabled — backward compatible) // WebRTC fields (zero values when WebRTC not enabled — backward compatible)
HasSFU bool `json:"has_sfu,omitempty"` HasSFU bool `json:"has_sfu,omitempty"`
HasTURN bool `json:"has_turn,omitempty"` HasTURN bool `json:"has_turn,omitempty"`
TURNSharedSecret string `json:"-"` // Never persisted to disk state file TURNSharedSecret string `json:"turn_shared_secret,omitempty"` // Needed for gateway to generate TURN credentials on cold start
TURNDomain string `json:"turn_domain,omitempty"` // TURN server domain for gateway config
TURNCredentialTTL int `json:"turn_credential_ttl,omitempty"` TURNCredentialTTL int `json:"turn_credential_ttl,omitempty"`
SFUSignalingPort int `json:"sfu_signaling_port,omitempty"` SFUSignalingPort int `json:"sfu_signaling_port,omitempty"`
SFUMediaPortStart int `json:"sfu_media_port_start,omitempty"` SFUMediaPortStart int `json:"sfu_media_port_start,omitempty"`
@ -1915,6 +1930,15 @@ func (cm *ClusterManager) restoreClusterFromState(ctx context.Context, state *Cl
IPFSTimeout: cm.ipfsTimeout, IPFSTimeout: cm.ipfsTimeout,
IPFSReplicationFactor: cm.ipfsReplicationFactor, IPFSReplicationFactor: cm.ipfsReplicationFactor,
} }
// Add WebRTC config from persisted local state
if state.HasSFU && state.SFUSignalingPort > 0 && state.TURNSharedSecret != "" {
gwCfg.WebRTCEnabled = true
gwCfg.SFUPort = state.SFUSignalingPort
gwCfg.TURNDomain = state.TURNDomain
gwCfg.TURNSecret = state.TURNSharedSecret
}
if err := cm.spawnGatewayWithSystemd(ctx, gwCfg); err != nil { if err := cm.spawnGatewayWithSystemd(ctx, gwCfg); err != nil {
cm.logger.Error("Failed to restore Gateway from state", zap.String("namespace", state.NamespaceName), zap.Error(err)) cm.logger.Error("Failed to restore Gateway from state", zap.String("namespace", state.NamespaceName), zap.Error(err))
} else { } else {

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/DeBrosOfficial/network/pkg/client" "github.com/DeBrosOfficial/network/pkg/client"
"github.com/DeBrosOfficial/network/pkg/gateway"
"github.com/DeBrosOfficial/network/pkg/sfu" "github.com/DeBrosOfficial/network/pkg/sfu"
"github.com/google/uuid" "github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
@ -189,7 +190,10 @@ func (cm *ClusterManager) EnableWebRTC(ctx context.Context, namespaceName, enabl
} }
// 14. Update cluster-state.json on all nodes with WebRTC info // 14. Update cluster-state.json on all nodes with WebRTC info
cm.updateClusterStateWithWebRTC(ctx, cluster, clusterNodes, sfuBlocks, turnBlocks) cm.updateClusterStateWithWebRTC(ctx, cluster, clusterNodes, sfuBlocks, turnBlocks, turnDomain, turnSecret)
// 15. Restart namespace gateways with WebRTC config so they register WebRTC routes
cm.restartGatewaysWithWebRTC(ctx, cluster, clusterNodes, nodePortBlocks, sfuBlocks, turnDomain, turnSecret)
cm.logEvent(ctx, cluster.ID, EventWebRTCEnabled, "", cm.logEvent(ctx, cluster.ID, EventWebRTCEnabled, "",
fmt.Sprintf("WebRTC enabled: SFU on %d nodes, TURN on %d nodes", len(clusterNodes), len(turnNodes)), nil) fmt.Sprintf("WebRTC enabled: SFU on %d nodes, TURN on %d nodes", len(clusterNodes), len(turnNodes)), nil)
@ -265,7 +269,19 @@ func (cm *ClusterManager) DisableWebRTC(ctx context.Context, namespaceName strin
cm.db.Exec(internalCtx, `DELETE FROM namespace_webrtc_config WHERE namespace_cluster_id = ?`, cluster.ID) cm.db.Exec(internalCtx, `DELETE FROM namespace_webrtc_config WHERE namespace_cluster_id = ?`, cluster.ID)
// 9. Update cluster-state.json to remove WebRTC info // 9. Update cluster-state.json to remove WebRTC info
cm.updateClusterStateWithWebRTC(ctx, cluster, clusterNodes, nil, nil) cm.updateClusterStateWithWebRTC(ctx, cluster, clusterNodes, nil, nil, "", "")
// 10. Restart namespace gateways without WebRTC config so they unregister WebRTC routes
portBlocks, err := cm.portAllocator.GetAllPortBlocks(ctx, cluster.ID)
if err == nil {
nodePortBlocks := make(map[string]*PortBlock)
for i := range portBlocks {
nodePortBlocks[portBlocks[i].NodeID] = &portBlocks[i]
}
cm.restartGatewaysWithWebRTC(ctx, cluster, clusterNodes, nodePortBlocks, nil, "", "")
} else {
cm.logger.Warn("Failed to get port blocks for gateway restart after WebRTC disable", zap.Error(err))
}
cm.logEvent(ctx, cluster.ID, EventWebRTCDisabled, "", "WebRTC disabled", nil) cm.logEvent(ctx, cluster.ID, EventWebRTCDisabled, "", "WebRTC disabled", nil)
@ -508,13 +524,14 @@ func (cm *ClusterManager) cleanupWebRTCOnError(ctx context.Context, clusterID, n
// updateClusterStateWithWebRTC updates the cluster-state.json on all nodes // updateClusterStateWithWebRTC updates the cluster-state.json on all nodes
// to include (or remove) WebRTC port information. // to include (or remove) WebRTC port information.
// Pass nil maps to clear WebRTC state (when disabling). // Pass nil maps and empty strings to clear WebRTC state (when disabling).
func (cm *ClusterManager) updateClusterStateWithWebRTC( func (cm *ClusterManager) updateClusterStateWithWebRTC(
ctx context.Context, ctx context.Context,
cluster *NamespaceCluster, cluster *NamespaceCluster,
nodes []clusterNodeInfo, nodes []clusterNodeInfo,
sfuBlocks map[string]*WebRTCPortBlock, sfuBlocks map[string]*WebRTCPortBlock,
turnBlocks map[string]*WebRTCPortBlock, turnBlocks map[string]*WebRTCPortBlock,
turnDomain, turnSecret string,
) { ) {
// Get existing port blocks for base state // Get existing port blocks for base state
portBlocks, err := cm.portAllocator.GetAllPortBlocks(ctx, cluster.ID) portBlocks, err := cm.portAllocator.GetAllPortBlocks(ctx, cluster.ID)
@ -589,6 +606,9 @@ func (cm *ClusterManager) updateClusterStateWithWebRTC(
state.TURNRelayPortEnd = turnBlock.TURNRelayPortEnd state.TURNRelayPortEnd = turnBlock.TURNRelayPortEnd
} }
} }
// Persist TURN domain and secret so gateways can be restored on cold start
state.TURNDomain = turnDomain
state.TURNSharedSecret = turnSecret
if node.NodeID == cm.localNodeID { if node.NodeID == cm.localNodeID {
if err := cm.saveLocalState(state); err != nil { if err := cm.saveLocalState(state); err != nil {
@ -615,3 +635,118 @@ func (cm *ClusterManager) saveRemoteState(ctx context.Context, nodeIP, namespace
zap.Error(err)) zap.Error(err))
} }
} }
// restartGatewaysWithWebRTC restarts namespace gateways on all nodes with updated WebRTC config.
// Pass nil sfuBlocks and empty turnDomain/turnSecret to disable WebRTC on gateways.
func (cm *ClusterManager) restartGatewaysWithWebRTC(
ctx context.Context,
cluster *NamespaceCluster,
nodes []clusterNodeInfo,
portBlocks map[string]*PortBlock,
sfuBlocks map[string]*WebRTCPortBlock,
turnDomain, turnSecret string,
) {
// Build Olric server addresses from port blocks + node IPs
var olricServers []string
for _, node := range nodes {
if pb, ok := portBlocks[node.NodeID]; ok {
olricServers = append(olricServers, fmt.Sprintf("%s:%d", node.InternalIP, pb.OlricHTTPPort))
}
}
for _, node := range nodes {
pb, ok := portBlocks[node.NodeID]
if !ok {
cm.logger.Warn("No port block for node, skipping gateway restart",
zap.String("node_id", node.NodeID))
continue
}
// Build gateway config with WebRTC fields
webrtcEnabled := false
sfuPort := 0
if sfuBlocks != nil {
if sfuBlock, ok := sfuBlocks[node.NodeID]; ok {
webrtcEnabled = true
sfuPort = sfuBlock.SFUSignalingPort
}
}
cfg := gateway.InstanceConfig{
Namespace: cluster.NamespaceName,
NodeID: node.NodeID,
HTTPPort: pb.GatewayHTTPPort,
BaseDomain: cm.baseDomain,
RQLiteDSN: fmt.Sprintf("http://localhost:%d", pb.RQLiteHTTPPort),
GlobalRQLiteDSN: cm.globalRQLiteDSN,
OlricServers: olricServers,
OlricTimeout: 30 * time.Second,
IPFSClusterAPIURL: cm.ipfsClusterAPIURL,
IPFSAPIURL: cm.ipfsAPIURL,
IPFSTimeout: cm.ipfsTimeout,
IPFSReplicationFactor: cm.ipfsReplicationFactor,
WebRTCEnabled: webrtcEnabled,
SFUPort: sfuPort,
TURNDomain: turnDomain,
TURNSecret: turnSecret,
}
if node.NodeID == cm.localNodeID {
if err := cm.systemdSpawner.RestartGateway(ctx, cluster.NamespaceName, node.NodeID, cfg); err != nil {
cm.logger.Error("Failed to restart local gateway with WebRTC config",
zap.String("namespace", cluster.NamespaceName),
zap.String("node_id", node.NodeID),
zap.Error(err))
} else {
cm.logger.Info("Restarted local gateway with WebRTC config",
zap.String("namespace", cluster.NamespaceName),
zap.Bool("webrtc_enabled", webrtcEnabled))
}
} else {
cm.restartGatewayRemote(ctx, node.InternalIP, cfg)
}
}
}
// restartGatewayRemote sends a restart-gateway request to a remote node.
func (cm *ClusterManager) restartGatewayRemote(ctx context.Context, nodeIP string, cfg gateway.InstanceConfig) {
ipfsTimeout := ""
if cfg.IPFSTimeout > 0 {
ipfsTimeout = cfg.IPFSTimeout.String()
}
olricTimeout := ""
if cfg.OlricTimeout > 0 {
olricTimeout = cfg.OlricTimeout.String()
}
_, err := cm.sendSpawnRequest(ctx, nodeIP, map[string]interface{}{
"action": "restart-gateway",
"namespace": cfg.Namespace,
"node_id": cfg.NodeID,
"gateway_http_port": cfg.HTTPPort,
"gateway_base_domain": cfg.BaseDomain,
"gateway_rqlite_dsn": cfg.RQLiteDSN,
"gateway_global_rqlite_dsn": cfg.GlobalRQLiteDSN,
"gateway_olric_servers": cfg.OlricServers,
"gateway_olric_timeout": olricTimeout,
"ipfs_cluster_api_url": cfg.IPFSClusterAPIURL,
"ipfs_api_url": cfg.IPFSAPIURL,
"ipfs_timeout": ipfsTimeout,
"ipfs_replication_factor": cfg.IPFSReplicationFactor,
"gateway_webrtc_enabled": cfg.WebRTCEnabled,
"gateway_sfu_port": cfg.SFUPort,
"gateway_turn_domain": cfg.TURNDomain,
"gateway_turn_secret": cfg.TURNSecret,
})
if err != nil {
cm.logger.Error("Failed to restart remote gateway with WebRTC config",
zap.String("node_ip", nodeIP),
zap.String("namespace", cfg.Namespace),
zap.Error(err))
} else {
cm.logger.Info("Restarted remote gateway with WebRTC config",
zap.String("node_ip", nodeIP),
zap.String("namespace", cfg.Namespace),
zap.Bool("webrtc_enabled", cfg.WebRTCEnabled))
}
}

View File

@ -529,6 +529,16 @@ func (cm *ClusterManager) ReplaceClusterNode(ctx context.Context, cluster *Names
IPFSReplicationFactor: cm.ipfsReplicationFactor, IPFSReplicationFactor: cm.ipfsReplicationFactor,
} }
// Add WebRTC config if enabled for this namespace
if webrtcCfg, err := cm.GetWebRTCConfig(ctx, cluster.NamespaceName); err == nil && webrtcCfg != nil {
if sfuBlock, err := cm.webrtcPortAllocator.GetSFUPorts(ctx, cluster.ID, replacement.NodeID); err == nil && sfuBlock != nil {
gwCfg.WebRTCEnabled = true
gwCfg.SFUPort = sfuBlock.SFUSignalingPort
gwCfg.TURNDomain = fmt.Sprintf("turn.ns-%s.%s", cluster.NamespaceName, cm.baseDomain)
gwCfg.TURNSecret = webrtcCfg.TURNSharedSecret
}
}
var spawnErr error var spawnErr error
if replacement.NodeID == cm.localNodeID { if replacement.NodeID == cm.localNodeID {
spawnErr = cm.spawnGatewayWithSystemd(ctx, gwCfg) spawnErr = cm.spawnGatewayWithSystemd(ctx, gwCfg)
@ -1061,6 +1071,16 @@ func (cm *ClusterManager) addNodeToCluster(
IPFSReplicationFactor: cm.ipfsReplicationFactor, IPFSReplicationFactor: cm.ipfsReplicationFactor,
} }
// Add WebRTC config if enabled for this namespace
if webrtcCfg, err := cm.GetWebRTCConfig(ctx, cluster.NamespaceName); err == nil && webrtcCfg != nil {
if sfuBlock, err := cm.webrtcPortAllocator.GetSFUPorts(ctx, cluster.ID, replacement.NodeID); err == nil && sfuBlock != nil {
gwCfg.WebRTCEnabled = true
gwCfg.SFUPort = sfuBlock.SFUSignalingPort
gwCfg.TURNDomain = fmt.Sprintf("turn.ns-%s.%s", cluster.NamespaceName, cm.baseDomain)
gwCfg.TURNSecret = webrtcCfg.TURNSharedSecret
}
}
if replacement.NodeID == cm.localNodeID { if replacement.NodeID == cm.localNodeID {
spawnErr = cm.spawnGatewayWithSystemd(ctx, gwCfg) spawnErr = cm.spawnGatewayWithSystemd(ctx, gwCfg)
} else { } else {

View File

@ -195,22 +195,8 @@ func (s *SystemdSpawner) SpawnGateway(ctx context.Context, namespace, nodeID str
configPath := filepath.Join(configDir, fmt.Sprintf("gateway-%s.yaml", nodeID)) configPath := filepath.Join(configDir, fmt.Sprintf("gateway-%s.yaml", nodeID))
// Build Gateway YAML config // Build Gateway YAML config using the shared type from gateway package
type gatewayYAMLConfig struct { gatewayConfig := gateway.GatewayYAMLConfig{
ListenAddr string `yaml:"listen_addr"`
ClientNamespace string `yaml:"client_namespace"`
RQLiteDSN string `yaml:"rqlite_dsn"`
GlobalRQLiteDSN string `yaml:"global_rqlite_dsn,omitempty"`
DomainName string `yaml:"domain_name"`
OlricServers []string `yaml:"olric_servers"`
OlricTimeout string `yaml:"olric_timeout"`
IPFSClusterAPIURL string `yaml:"ipfs_cluster_api_url"`
IPFSAPIURL string `yaml:"ipfs_api_url"`
IPFSTimeout string `yaml:"ipfs_timeout"`
IPFSReplicationFactor int `yaml:"ipfs_replication_factor"`
}
gatewayConfig := gatewayYAMLConfig{
ListenAddr: fmt.Sprintf(":%d", cfg.HTTPPort), ListenAddr: fmt.Sprintf(":%d", cfg.HTTPPort),
ClientNamespace: cfg.Namespace, ClientNamespace: cfg.Namespace,
RQLiteDSN: cfg.RQLiteDSN, RQLiteDSN: cfg.RQLiteDSN,
@ -222,6 +208,12 @@ func (s *SystemdSpawner) SpawnGateway(ctx context.Context, namespace, nodeID str
IPFSAPIURL: cfg.IPFSAPIURL, IPFSAPIURL: cfg.IPFSAPIURL,
IPFSTimeout: cfg.IPFSTimeout.String(), IPFSTimeout: cfg.IPFSTimeout.String(),
IPFSReplicationFactor: cfg.IPFSReplicationFactor, IPFSReplicationFactor: cfg.IPFSReplicationFactor,
WebRTC: gateway.GatewayYAMLWebRTC{
Enabled: cfg.WebRTCEnabled,
SFUPort: cfg.SFUPort,
TURNDomain: cfg.TURNDomain,
TURNSecret: cfg.TURNSecret,
},
} }
configBytes, err := yaml.Marshal(gatewayConfig) configBytes, err := yaml.Marshal(gatewayConfig)
@ -291,6 +283,24 @@ func (s *SystemdSpawner) StopGateway(ctx context.Context, namespace, nodeID stri
return s.systemdMgr.StopService(namespace, systemd.ServiceTypeGateway) return s.systemdMgr.StopService(namespace, systemd.ServiceTypeGateway)
} }
// RestartGateway stops and re-spawns a Gateway instance with updated config.
// Used when gateway config changes at runtime (e.g., WebRTC enable/disable).
func (s *SystemdSpawner) RestartGateway(ctx context.Context, namespace, nodeID string, cfg gateway.InstanceConfig) error {
s.logger.Info("Restarting Gateway via systemd",
zap.String("namespace", namespace),
zap.String("node_id", nodeID))
// Stop existing service (ignore error if already stopped)
if err := s.systemdMgr.StopService(namespace, systemd.ServiceTypeGateway); err != nil {
s.logger.Warn("Failed to stop Gateway before restart (may not be running)",
zap.String("namespace", namespace),
zap.Error(err))
}
// Re-spawn with updated config
return s.SpawnGateway(ctx, namespace, nodeID, cfg)
}
// SFUInstanceConfig holds configuration for spawning an SFU instance // SFUInstanceConfig holds configuration for spawning an SFU instance
type SFUInstanceConfig struct { type SFUInstanceConfig struct {
Namespace string Namespace string