import * as plugins from '../plugins.js'; import { logger } from '../logger.js'; import { ProxyCertDoc } from '../db/index.js'; import { RemoteIngressManager, type IRemoteIngressFirewallConfig } from './classes.remoteingress-manager.js'; import { TunnelManager } from './classes.tunnel-manager.js'; import type { IDcRouterRouteConfig, IRemoteIngressHubSettings, TRemoteIngressHubSettingsUpdate } from '../../ts_interfaces/data/remoteingress.js'; import type { DcRouter } from '../classes.dcrouter.js'; /** * Generation-guarded lifecycle for the RemoteIngress tunnel hub: serialized * start/stop/restart of the Rust hub, edge mutations, route/firewall pushes, * and hub-settings updates that may require a SmartProxy restart. */ export class RemoteIngressHubLifecycle { private lifecycleChain: Promise = Promise.resolve(); private stopping = false; private generation = 0; constructor(private dcRouterRef: DcRouter) {} public async setup(): Promise { const remoteIngressManager = this.dcRouterRef.remoteIngressManager; if (!remoteIngressManager) { return; } const hubSettings = remoteIngressManager.getHubSettings(); if (!hubSettings.enabled) { logger.log('info', 'Remote Ingress hub is disabled in DB settings'); return; } logger.log('info', 'Setting up Remote Ingress hub...'); this.stopping = false; const generation = ++this.generation; const firewallConfig = await this.dcRouterRef.securityPolicyManager?.compileRemoteIngressFirewall(); if (!this.isGenerationCurrent(generation, remoteIngressManager)) { return; } remoteIngressManager.setFirewallConfig(firewallConfig); // Pass current bootstrap routes so the manager can derive edge ports initially. // Once RouteConfigManager applies the full DB set, the onRoutesApplied callback // will push the complete merged routes here. remoteIngressManager.setRoutes(this.dcRouterRef.getRemoteIngressBootstrapRoutes() as any[]); // If ConfigManagers finished before us, re-apply routes // so the callback delivers the full DB set to our newly-created remoteIngressManager. if (this.dcRouterRef.routeConfigManager) { await this.dcRouterRef.routeConfigManager.applyRoutes(); } if (!this.isGenerationCurrent(generation, remoteIngressManager)) { return; } await this.queueTask(async () => { await this.startTunnelHubLocked(generation); }); if (!this.isGenerationCurrent(generation, remoteIngressManager)) { return; } const edgeCount = remoteIngressManager.getAllEdges().length; logger.log('info', `Remote Ingress hub started on port ${hubSettings.tunnelPort} with ${edgeCount} registered edge(s)`); } public async stop(): Promise { this.stopping = true; this.generation++; await this.queueTask(async () => { const currentTunnelManager = this.dcRouterRef.tunnelManager; if (currentTunnelManager) { await currentTunnelManager.stop(); if (this.dcRouterRef.tunnelManager === currentTunnelManager) { this.dcRouterRef.tunnelManager = undefined; } } }); } public async mutateEdges( mutation: (manager: RemoteIngressManager) => Promise, syncAllowedEdges = true, ): Promise { return await this.queueTask(async () => { if (this.stopping) { throw new Error('RemoteIngress is stopping'); } const manager = this.dcRouterRef.remoteIngressManager; if (!manager) { throw new Error('RemoteIngress not configured'); } const result = await mutation(manager); if (syncAllowedEdges && this.dcRouterRef.tunnelManager) { await this.dcRouterRef.tunnelManager.syncAllowedEdges(); } return result; }); } public async updateRoutes(routes: IDcRouterRouteConfig[]): Promise { await this.queueTask(async () => { if (this.stopping) return; if (this.dcRouterRef.remoteIngressManager) { this.dcRouterRef.remoteIngressManager.setRoutes(routes); } if (this.dcRouterRef.tunnelManager) { await this.dcRouterRef.tunnelManager.syncAllowedEdges(); } }); } public async applyFirewallConfig(firewallConfig: IRemoteIngressFirewallConfig | undefined): Promise { await this.queueTask(async () => { if (this.stopping) return; if (this.dcRouterRef.remoteIngressManager) { this.dcRouterRef.remoteIngressManager.setFirewallConfig(firewallConfig); } if (this.dcRouterRef.tunnelManager) { await this.dcRouterRef.tunnelManager.syncAllowedEdges(); } }); } public async updateHubSettings( updates: TRemoteIngressHubSettingsUpdate, updatedBy: string, ): Promise { const manager = this.dcRouterRef.remoteIngressManager; if (!manager) { throw new Error('RemoteIngress is not configured'); } const previousSettings = manager.getHubSettings(); const settings = await manager.updateHubSettings(updates, updatedBy); const enabledChanged = previousSettings.enabled !== settings.enabled; if (!settings.enabled) { await this.queueTask(async () => { await this.stopTunnelHubLocked(); }); } if (enabledChanged) { await this.dcRouterRef.restartSmartProxyForRemoteIngressSettings(); } if (settings.enabled) { await this.queueTask(async () => { await this.restartTunnelHubLocked(); }); } return settings; } private isGenerationCurrent(generation: number, manager: RemoteIngressManager): boolean { return !this.stopping && generation === this.generation && this.dcRouterRef.remoteIngressManager === manager; } private queueTask(task: () => Promise): Promise { const run = this.lifecycleChain.then(task); this.lifecycleChain = run.then(() => undefined, () => undefined); return run; } private async stopTunnelHubLocked(): Promise { this.generation++; const currentTunnelManager = this.dcRouterRef.tunnelManager; if (currentTunnelManager) { await currentTunnelManager.stop(); if (this.dcRouterRef.tunnelManager === currentTunnelManager) { this.dcRouterRef.tunnelManager = undefined; } } this.dcRouterRef.mailDnsSync.requestEdgeEligibilityCheck('RemoteIngress hub stopped'); } private async restartTunnelHubLocked(): Promise { const generation = ++this.generation; const hubSettings = this.dcRouterRef.remoteIngressManager?.getHubSettings(); if (!this.dcRouterRef.remoteIngressManager || !hubSettings?.enabled || this.stopping) { return; } const currentTunnelManager = this.dcRouterRef.tunnelManager; if (currentTunnelManager) { await currentTunnelManager.stop(); if (this.dcRouterRef.tunnelManager === currentTunnelManager) { this.dcRouterRef.tunnelManager = undefined; } } if (this.stopping || generation !== this.generation) { return; } await this.startTunnelHubLocked(generation); } private async startTunnelHubLocked(generation: number): Promise { const manager = this.dcRouterRef.remoteIngressManager; const hubSettings = manager?.getHubSettings(); if (!manager || !hubSettings?.enabled || this.stopping || generation !== this.generation) { return; } const firewallConfig = await this.dcRouterRef.securityPolicyManager?.compileRemoteIngressFirewall(); if (this.stopping || generation !== this.generation || this.dcRouterRef.remoteIngressManager !== manager) { return; } manager.setFirewallConfig(firewallConfig); const tlsConfig = await this.resolveTlsConfig(hubSettings.hubDomain); if (this.stopping || generation !== this.generation || this.dcRouterRef.remoteIngressManager !== manager) { return; } const tunnelManager = new TunnelManager(manager, { tunnelPort: hubSettings.tunnelPort, targetHost: '127.0.0.1', tls: tlsConfig, performance: manager.getHubPerformanceConfig(), }); tunnelManager.setOnTopologyChanged((reason) => ( this.dcRouterRef.mailDnsSync.requestEdgeEligibilityCheck(reason) )); try { await tunnelManager.start(); } catch (err) { await tunnelManager.stop().catch(() => {}); throw err; } if (this.stopping || generation !== this.generation || this.dcRouterRef.remoteIngressManager !== manager) { await tunnelManager.stop().catch((err) => { logger.log('warn', `Failed to stop stale RemoteIngress tunnel hub: ${(err as Error).message}`); }); return; } this.dcRouterRef.tunnelManager = tunnelManager; } private async resolveTlsConfig( hubDomain?: string, ): Promise<{ certPem: string; keyPem: string } | undefined> { // Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default) let tlsConfig: { certPem: string; keyPem: string } | undefined; // Priority 1: Explicit cert/key file paths const explicitTls = this.dcRouterRef.options.remoteIngressConfig?.tls; if (explicitTls?.certPath && explicitTls?.keyPath) { try { const certPem = plugins.fs.readFileSync(explicitTls.certPath, 'utf8'); const keyPem = plugins.fs.readFileSync(explicitTls.keyPath, 'utf8'); tlsConfig = { certPem, keyPem }; logger.log('info', 'Using explicit TLS cert/key for RemoteIngress tunnel'); } catch (err: unknown) { logger.log('warn', `Failed to read RemoteIngress TLS cert/key files: ${(err as Error).message}`); } } // Priority 2: Existing cert from SmartProxy cert store for hubDomain if (!tlsConfig && hubDomain) { try { const stored = await ProxyCertDoc.findByDomain(hubDomain); if (stored?.publicKey && stored?.privateKey) { tlsConfig = { certPem: stored.publicKey, keyPem: stored.privateKey }; logger.log('info', `Using stored ACME cert for RemoteIngress tunnel TLS: ${hubDomain}`); } } catch { /* no stored cert, fall through */ } } if (!tlsConfig) { logger.log('info', 'No TLS cert configured for RemoteIngress tunnel — using auto-generated self-signed'); } return tlsConfig; } }