diff --git a/.gitignore b/.gitignore index 822f057..88e1e40 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,4 @@ terms-agreement ./cli ./inspector docs/later_todos/ +sim/ \ No newline at end of file diff --git a/pkg/gateway/handlers/namespace/spawn_handler.go b/pkg/gateway/handlers/namespace/spawn_handler.go index 6d9b2da..c070be7 100644 --- a/pkg/gateway/handlers/namespace/spawn_handler.go +++ b/pkg/gateway/handlers/namespace/spawn_handler.go @@ -48,6 +48,11 @@ type SpawnRequest struct { IPFSAPIURL string `json:"ipfs_api_url,omitempty"` IPFSTimeout string `json:"ipfs_timeout,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") SFUListenAddr string `json:"sfu_listen_addr,omitempty"` @@ -225,6 +230,10 @@ func (h *SpawnHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { IPFSAPIURL: req.IPFSAPIURL, IPFSTimeout: ipfsTimeout, 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 { 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}) + 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": if len(req.ClusterState) == 0 { writeSpawnResponse(w, http.StatusBadRequest, SpawnResponse{Error: "cluster_state is required"}) diff --git a/pkg/gateway/instance_spawner.go b/pkg/gateway/instance_spawner.go index 77eea8f..a3d56dd 100644 --- a/pkg/gateway/instance_spawner.go +++ b/pkg/gateway/instance_spawner.go @@ -90,26 +90,41 @@ type InstanceConfig struct { IPFSAPIURL string // IPFS API URL (e.g., "http://localhost:5001") IPFSTimeout time.Duration // Timeout for IPFS operations 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 // This must match the yamlCfg struct in cmd/gateway/config.go exactly // because the gateway uses strict YAML decoding that rejects unknown fields type GatewayYAMLConfig struct { - ListenAddr string `yaml:"listen_addr"` - ClientNamespace string `yaml:"client_namespace"` - RQLiteDSN string `yaml:"rqlite_dsn"` - GlobalRQLiteDSN string `yaml:"global_rqlite_dsn,omitempty"` - BootstrapPeers []string `yaml:"bootstrap_peers,omitempty"` - EnableHTTPS bool `yaml:"enable_https,omitempty"` - DomainName string `yaml:"domain_name,omitempty"` - TLSCacheDir string `yaml:"tls_cache_dir,omitempty"` - OlricServers []string `yaml:"olric_servers"` - OlricTimeout string `yaml:"olric_timeout,omitempty"` - IPFSClusterAPIURL string `yaml:"ipfs_cluster_api_url,omitempty"` - IPFSAPIURL string `yaml:"ipfs_api_url,omitempty"` - IPFSTimeout string `yaml:"ipfs_timeout,omitempty"` - IPFSReplicationFactor int `yaml:"ipfs_replication_factor,omitempty"` + ListenAddr string `yaml:"listen_addr"` + ClientNamespace string `yaml:"client_namespace"` + RQLiteDSN string `yaml:"rqlite_dsn"` + GlobalRQLiteDSN string `yaml:"global_rqlite_dsn,omitempty"` + BootstrapPeers []string `yaml:"bootstrap_peers,omitempty"` + EnableHTTPS bool `yaml:"enable_https,omitempty"` + DomainName string `yaml:"domain_name,omitempty"` + TLSCacheDir string `yaml:"tls_cache_dir,omitempty"` + OlricServers []string `yaml:"olric_servers"` + OlricTimeout string `yaml:"olric_timeout,omitempty"` + IPFSClusterAPIURL string `yaml:"ipfs_cluster_api_url,omitempty"` + IPFSAPIURL string `yaml:"ipfs_api_url,omitempty"` + IPFSTimeout string `yaml:"ipfs_timeout,omitempty"` + IPFSReplicationFactor int `yaml:"ipfs_replication_factor,omitempty"` + WebRTC GatewayYAMLWebRTC `yaml:"webrtc,omitempty"` } // NewInstanceSpawner creates a new Gateway instance spawner @@ -294,6 +309,12 @@ func (is *InstanceSpawner) generateConfig(configPath string, cfg InstanceConfig, IPFSClusterAPIURL: cfg.IPFSClusterAPIURL, IPFSAPIURL: cfg.IPFSAPIURL, IPFSReplicationFactor: cfg.IPFSReplicationFactor, + WebRTC: GatewayYAMLWebRTC{ + Enabled: cfg.WebRTCEnabled, + SFUPort: cfg.SFUPort, + TURNDomain: cfg.TURNDomain, + TURNSecret: cfg.TURNSecret, + }, } // Set Olric timeout if provided if cfg.OlricTimeout > 0 { diff --git a/pkg/namespace/cluster_manager.go b/pkg/namespace/cluster_manager.go index 0193075..11350e4 100644 --- a/pkg/namespace/cluster_manager.go +++ b/pkg/namespace/cluster_manager.go @@ -661,6 +661,10 @@ func (cm *ClusterManager) spawnGatewayRemote(ctx context.Context, nodeIP string, "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 { return nil, err @@ -1555,6 +1559,16 @@ func (cm *ClusterManager) restoreClusterOnNode(ctx context.Context, clusterID, n 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 { cm.logger.Error("Failed to restore Gateway", zap.String("namespace", namespaceName), zap.Error(err)) } else { @@ -1617,7 +1631,8 @@ type ClusterLocalState struct { // WebRTC fields (zero values when WebRTC not enabled — backward compatible) HasSFU bool `json:"has_sfu,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"` SFUSignalingPort int `json:"sfu_signaling_port,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, 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 { cm.logger.Error("Failed to restore Gateway from state", zap.String("namespace", state.NamespaceName), zap.Error(err)) } else { diff --git a/pkg/namespace/cluster_manager_webrtc.go b/pkg/namespace/cluster_manager_webrtc.go index 2acf298..bd7bfba 100644 --- a/pkg/namespace/cluster_manager_webrtc.go +++ b/pkg/namespace/cluster_manager_webrtc.go @@ -8,6 +8,7 @@ import ( "time" "github.com/DeBrosOfficial/network/pkg/client" + "github.com/DeBrosOfficial/network/pkg/gateway" "github.com/DeBrosOfficial/network/pkg/sfu" "github.com/google/uuid" "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 - 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, "", 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) // 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) @@ -508,13 +524,14 @@ func (cm *ClusterManager) cleanupWebRTCOnError(ctx context.Context, clusterID, n // updateClusterStateWithWebRTC updates the cluster-state.json on all nodes // 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( ctx context.Context, cluster *NamespaceCluster, nodes []clusterNodeInfo, sfuBlocks map[string]*WebRTCPortBlock, turnBlocks map[string]*WebRTCPortBlock, + turnDomain, turnSecret string, ) { // Get existing port blocks for base state portBlocks, err := cm.portAllocator.GetAllPortBlocks(ctx, cluster.ID) @@ -589,6 +606,9 @@ func (cm *ClusterManager) updateClusterStateWithWebRTC( 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 err := cm.saveLocalState(state); err != nil { @@ -615,3 +635,118 @@ func (cm *ClusterManager) saveRemoteState(ctx context.Context, nodeIP, namespace 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)) + } +} diff --git a/pkg/namespace/cluster_recovery.go b/pkg/namespace/cluster_recovery.go index c3b1899..cdc2467 100644 --- a/pkg/namespace/cluster_recovery.go +++ b/pkg/namespace/cluster_recovery.go @@ -529,6 +529,16 @@ func (cm *ClusterManager) ReplaceClusterNode(ctx context.Context, cluster *Names 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 if replacement.NodeID == cm.localNodeID { spawnErr = cm.spawnGatewayWithSystemd(ctx, gwCfg) @@ -1061,6 +1071,16 @@ func (cm *ClusterManager) addNodeToCluster( 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 { spawnErr = cm.spawnGatewayWithSystemd(ctx, gwCfg) } else { diff --git a/pkg/namespace/systemd_spawner.go b/pkg/namespace/systemd_spawner.go index a8dbbfc..22ea196 100644 --- a/pkg/namespace/systemd_spawner.go +++ b/pkg/namespace/systemd_spawner.go @@ -195,22 +195,8 @@ func (s *SystemdSpawner) SpawnGateway(ctx context.Context, namespace, nodeID str configPath := filepath.Join(configDir, fmt.Sprintf("gateway-%s.yaml", nodeID)) - // Build Gateway YAML config - type gatewayYAMLConfig struct { - 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{ + // Build Gateway YAML config using the shared type from gateway package + gatewayConfig := gateway.GatewayYAMLConfig{ ListenAddr: fmt.Sprintf(":%d", cfg.HTTPPort), ClientNamespace: cfg.Namespace, RQLiteDSN: cfg.RQLiteDSN, @@ -222,6 +208,12 @@ func (s *SystemdSpawner) SpawnGateway(ctx context.Context, namespace, nodeID str IPFSAPIURL: cfg.IPFSAPIURL, IPFSTimeout: cfg.IPFSTimeout.String(), IPFSReplicationFactor: cfg.IPFSReplicationFactor, + WebRTC: gateway.GatewayYAMLWebRTC{ + Enabled: cfg.WebRTCEnabled, + SFUPort: cfg.SFUPort, + TURNDomain: cfg.TURNDomain, + TURNSecret: cfg.TURNSecret, + }, } 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) } +// 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 type SFUInstanceConfig struct { Namespace string