import * as plugins from '../../plugins.js'; import { logger } from '../../core/utils/logger.js'; import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js'; // Importing required components import { ConnectionManager } from './connection-manager.js'; import { SecurityManager } from './security-manager.js'; import { TlsManager } from './tls-manager.js'; import { HttpProxyBridge } from './http-proxy-bridge.js'; import { TimeoutManager } from './timeout-manager.js'; import { PortManager } from './port-manager.js'; import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js'; import { RouteConnectionHandler } from './route-connection-handler.js'; import { NFTablesManager } from './nftables-manager.js'; // Certificate manager import { SmartCertManager, type ICertStatus } from './certificate-manager.js'; // Import types and utilities import type { ISmartProxyOptions } from './models/interfaces.js'; import type { IRouteConfig } from './models/route-types.js'; // Import mutex for route update synchronization import { Mutex } from './utils/mutex.js'; // Import route validator import { RouteValidator } from './utils/route-validator.js'; // Import route orchestrator for route management import { RouteOrchestrator } from './route-orchestrator.js'; // Import ACME state manager import { AcmeStateManager } from './acme-state-manager.js'; // Import metrics collector import { MetricsCollector } from './metrics-collector.js'; import type { IMetrics } from './models/metrics-types.js'; /** * SmartProxy - Pure route-based API * * SmartProxy is a unified proxy system that works with routes to define connection handling behavior. * Each route contains matching criteria (ports, domains, etc.) and an action to take (forward, redirect, block). * * Configuration is provided through a set of routes, with each route defining: * - What to match (ports, domains, paths, client IPs) * - What to do with matching traffic (forward, redirect, block) * - How to handle TLS (passthrough, terminate, terminate-and-reencrypt) * - Security settings (IP restrictions, connection limits) * - Advanced options (timeout, headers, etc.) */ export class SmartProxy extends plugins.EventEmitter { // Port manager handles dynamic listener management private portManager: PortManager; private connectionLogger: NodeJS.Timeout | null = null; private isShuttingDown: boolean = false; // Component managers public connectionManager: ConnectionManager; public securityManager: SecurityManager; public tlsManager: TlsManager; public httpProxyBridge: HttpProxyBridge; public timeoutManager: TimeoutManager; public routeManager: RouteManager; public routeConnectionHandler: RouteConnectionHandler; public nftablesManager: NFTablesManager; // Certificate manager for ACME and static certificates public certManager: SmartCertManager | null = null; // Global challenge route tracking private globalChallengeRouteActive: boolean = false; private routeUpdateLock: Mutex; public acmeStateManager: AcmeStateManager; // Metrics collector public metricsCollector: MetricsCollector; // Route orchestrator for managing route updates private routeOrchestrator: RouteOrchestrator; // Track port usage across route updates private portUsageMap: Map> = new Map(); /** * Constructor for SmartProxy * * @param settingsArg Configuration options containing routes and other settings * Routes define how traffic is matched and handled, with each route having: * - match: criteria for matching traffic (ports, domains, paths, IPs) * - action: what to do with matched traffic (forward, redirect, block) * * Example: * ```ts * const proxy = new SmartProxy({ * routes: [ * { * match: { * ports: 443, * domains: ['example.com', '*.example.com'] * }, * action: { * type: 'forward', * target: { host: '10.0.0.1', port: 8443 }, * tls: { mode: 'passthrough' } * } * } * ], * defaults: { * target: { host: 'localhost', port: 8080 }, * security: { ipAllowList: ['*'] } * } * }); * ``` */ constructor(settingsArg: ISmartProxyOptions) { super(); // Set reasonable defaults for all settings this.settings = { ...settingsArg, initialDataTimeout: settingsArg.initialDataTimeout || 120000, socketTimeout: settingsArg.socketTimeout || 3600000, inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, maxConnectionLifetime: settingsArg.maxConnectionLifetime || 86400000, inactivityTimeout: settingsArg.inactivityTimeout || 14400000, gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true, keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true, keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, disableInactivityCheck: settingsArg.disableInactivityCheck || false, enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, enableDetailedLogging: settingsArg.enableDetailedLogging || false, enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, allowSessionTicket: settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true, maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, httpProxyPort: settingsArg.httpProxyPort || 8443, }; // Normalize ACME options if provided (support both email and accountEmail) if (this.settings.acme) { // Support both 'email' and 'accountEmail' fields if (this.settings.acme.accountEmail && !this.settings.acme.email) { this.settings.acme.email = this.settings.acme.accountEmail; } // Set reasonable defaults for commonly used fields this.settings.acme = { enabled: this.settings.acme.enabled !== false, // Enable by default if acme object exists port: this.settings.acme.port || 80, email: this.settings.acme.email, useProduction: this.settings.acme.useProduction || false, renewThresholdDays: this.settings.acme.renewThresholdDays || 30, autoRenew: this.settings.acme.autoRenew !== false, // Enable by default certificateStore: this.settings.acme.certificateStore || './certs', skipConfiguredCerts: this.settings.acme.skipConfiguredCerts || false, renewCheckIntervalHours: this.settings.acme.renewCheckIntervalHours || 24, routeForwards: this.settings.acme.routeForwards || [], ...this.settings.acme // Preserve any additional fields }; } // Initialize component managers this.timeoutManager = new TimeoutManager(this); this.securityManager = new SecurityManager(this); this.connectionManager = new ConnectionManager(this); // Create the route manager with SharedRouteManager API // Create a logger adapter to match ILogger interface const loggerAdapter = { debug: (message: string, data?: any) => logger.log('debug', message, data), info: (message: string, data?: any) => logger.log('info', message, data), warn: (message: string, data?: any) => logger.log('warn', message, data), error: (message: string, data?: any) => logger.log('error', message, data) }; // Validate initial routes if (this.settings.routes && this.settings.routes.length > 0) { const validation = RouteValidator.validateRoutes(this.settings.routes); if (!validation.valid) { RouteValidator.logValidationErrors(validation.errors); throw new Error(`Initial route validation failed: ${validation.errors.size} route(s) have errors`); } } this.routeManager = new RouteManager({ logger: loggerAdapter, enableDetailedLogging: this.settings.enableDetailedLogging, routes: this.settings.routes }); // Create other required components this.tlsManager = new TlsManager(this); this.httpProxyBridge = new HttpProxyBridge(this); // Initialize connection handler with route support this.routeConnectionHandler = new RouteConnectionHandler(this); // Initialize port manager this.portManager = new PortManager(this); // Initialize NFTablesManager this.nftablesManager = new NFTablesManager(this); // Initialize route update mutex for synchronization this.routeUpdateLock = new Mutex(); // Initialize ACME state manager this.acmeStateManager = new AcmeStateManager(); // Initialize metrics collector with reference to this SmartProxy instance this.metricsCollector = new MetricsCollector(this, { sampleIntervalMs: this.settings.metrics?.sampleIntervalMs, retentionSeconds: this.settings.metrics?.retentionSeconds }); // Initialize route orchestrator for managing route updates this.routeOrchestrator = new RouteOrchestrator( this.portManager, this.routeManager, this.httpProxyBridge, this.nftablesManager, null, // certManager will be set later loggerAdapter ); } /** * The settings for the SmartProxy */ public settings: ISmartProxyOptions; /** * Helper method to create and configure certificate manager * This ensures consistent setup including the required ACME callback */ private async createCertificateManager( routes: IRouteConfig[], certStore: string = './certs', acmeOptions?: any, initialState?: { challengeRouteActive?: boolean } ): Promise { const certManager = new SmartCertManager(routes, certStore, acmeOptions, initialState); // Always set up the route update callback for ACME challenges certManager.setUpdateRoutesCallback(async (routes) => { await this.updateRoutes(routes); }); // Connect with HttpProxy if available if (this.httpProxyBridge.getHttpProxy()) { certManager.setHttpProxy(this.httpProxyBridge.getHttpProxy()); } // Set the ACME state manager certManager.setAcmeStateManager(this.acmeStateManager); // Pass down the global ACME config if available if (this.settings.acme) { certManager.setGlobalAcmeDefaults(this.settings.acme); } // Pass down the custom certificate provision function if available if (this.settings.certProvisionFunction) { certManager.setCertProvisionFunction(this.settings.certProvisionFunction); } // Pass down the fallback to ACME setting if (this.settings.certProvisionFallbackToAcme !== undefined) { certManager.setCertProvisionFallbackToAcme(this.settings.certProvisionFallbackToAcme); } await certManager.initialize(); return certManager; } /** * Initialize certificate manager */ private async initializeCertificateManager(): Promise { // Extract global ACME options if any routes use auto certificates const autoRoutes = this.settings.routes.filter(r => r.action.tls?.certificate === 'auto' ); if (autoRoutes.length === 0 && !this.hasStaticCertRoutes()) { logger.log('info', 'No routes require certificate management', { component: 'certificate-manager' }); return; } // Prepare ACME options with priority: // 1. Use top-level ACME config if available // 2. Fall back to first auto route's ACME config // 3. Otherwise use undefined let acmeOptions: { email?: string; useProduction?: boolean; port?: number } | undefined; if (this.settings.acme?.email) { // Use top-level ACME config acmeOptions = { email: this.settings.acme.email, useProduction: this.settings.acme.useProduction || false, port: this.settings.acme.port || 80 }; logger.log('info', `Using top-level ACME configuration with email: ${acmeOptions.email}`, { component: 'certificate-manager' }); } else if (autoRoutes.length > 0) { // Check for route-level ACME config const routeWithAcme = autoRoutes.find(r => r.action.tls?.acme?.email); if (routeWithAcme?.action.tls?.acme) { const routeAcme = routeWithAcme.action.tls.acme; acmeOptions = { email: routeAcme.email, useProduction: routeAcme.useProduction || false, port: routeAcme.challengePort || 80 }; logger.log('info', `Using route-level ACME configuration from route '${routeWithAcme.name}' with email: ${acmeOptions.email}`, { component: 'certificate-manager' }); } } // Validate we have required configuration if (autoRoutes.length > 0 && !acmeOptions?.email) { throw new Error( 'ACME email is required for automatic certificate provisioning. ' + 'Please provide email in either:\n' + '1. Top-level "acme" configuration\n' + '2. Individual route\'s "tls.acme" configuration' ); } // Use the helper method to create and configure the certificate manager this.certManager = await this.createCertificateManager( this.settings.routes, this.settings.acme?.certificateStore || './certs', acmeOptions ); } /** * Check if we have routes with static certificates */ private hasStaticCertRoutes(): boolean { return this.settings.routes.some(r => r.action.tls?.certificate && r.action.tls.certificate !== 'auto' ); } /** * Start the proxy server with support for both configuration types */ public async start() { // Don't start if already shutting down if (this.isShuttingDown) { logger.log('warn', "Cannot start SmartProxy while it's in the shutdown process"); return; } // Validate the route configuration const configWarnings = this.routeManager.validateConfiguration(); // Also validate ACME configuration const acmeWarnings = this.validateAcmeConfiguration(); const allWarnings = [...configWarnings, ...acmeWarnings]; if (allWarnings.length > 0) { logger.log('warn', `${allWarnings.length} configuration warnings found`, { count: allWarnings.length }); for (const warning of allWarnings) { logger.log('warn', `${warning}`); } } // Get listening ports from RouteManager const listeningPorts = this.routeManager.getListeningPorts(); // Initialize port usage tracking using RouteOrchestrator this.portUsageMap = this.routeOrchestrator.updatePortUsageMap(this.settings.routes); // Log port usage for startup logger.log('info', `SmartProxy starting with ${listeningPorts.length} ports: ${listeningPorts.join(', ')}`, { portCount: listeningPorts.length, ports: listeningPorts, component: 'smart-proxy' }); // Provision NFTables rules for routes that use NFTables for (const route of this.settings.routes) { if (route.action.forwardingEngine === 'nftables') { await this.nftablesManager.provisionRoute(route); } } // Initialize and start HttpProxy if needed - before port binding if (this.settings.useHttpProxy && this.settings.useHttpProxy.length > 0) { await this.httpProxyBridge.initialize(); await this.httpProxyBridge.start(); } // Start port listeners using the PortManager BEFORE initializing certificate manager // This ensures all required ports are bound and ready when adding ACME challenge routes await this.portManager.addPorts(listeningPorts); // Initialize certificate manager AFTER port binding is complete // This ensures the ACME challenge port is already bound and ready when needed await this.initializeCertificateManager(); // Connect certificate manager with HttpProxy if both are available if (this.certManager && this.httpProxyBridge.getHttpProxy()) { this.certManager.setHttpProxy(this.httpProxyBridge.getHttpProxy()); } // Now that ports are listening, provision any required certificates if (this.certManager) { logger.log('info', 'Starting certificate provisioning now that ports are ready', { component: 'certificate-manager' }); await this.certManager.provisionAllCertificates(); } // Start the metrics collector now that all components are initialized this.metricsCollector.start(); // Set up periodic connection logging and inactivity checks this.connectionLogger = setInterval(() => { // Immediately return if shutting down if (this.isShuttingDown) return; // Perform inactivity check this.connectionManager.performInactivityCheck(); // Log connection statistics const now = Date.now(); let maxIncoming = 0; let maxOutgoing = 0; let tlsConnections = 0; let nonTlsConnections = 0; let completedTlsHandshakes = 0; let pendingTlsHandshakes = 0; let keepAliveConnections = 0; let httpProxyConnections = 0; // Get connection records for analysis const connectionRecords = this.connectionManager.getConnections(); // Analyze active connections for (const record of connectionRecords.values()) { // Track connection stats if (record.isTLS) { tlsConnections++; if (record.tlsHandshakeComplete) { completedTlsHandshakes++; } else { pendingTlsHandshakes++; } } else { nonTlsConnections++; } if (record.hasKeepAlive) { keepAliveConnections++; } if (record.usingNetworkProxy) { httpProxyConnections++; } maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime); if (record.outgoingStartTime) { maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime); } } // Get termination stats const terminationStats = this.connectionManager.getTerminationStats(); // Log detailed stats logger.log('info', 'Connection statistics', { activeConnections: connectionRecords.size, tls: { total: tlsConnections, completed: completedTlsHandshakes, pending: pendingTlsHandshakes }, nonTls: nonTlsConnections, keepAlive: keepAliveConnections, httpProxy: httpProxyConnections, longestRunning: { incoming: plugins.prettyMs(maxIncoming), outgoing: plugins.prettyMs(maxOutgoing) }, terminationStats: { incoming: terminationStats.incoming, outgoing: terminationStats.outgoing }, component: 'connection-manager' }); }, this.settings.inactivityCheckInterval || 60000); // Make sure the interval doesn't keep the process alive if (this.connectionLogger.unref) { this.connectionLogger.unref(); } } /** * Extract domain configurations from routes for certificate provisioning * * Note: This method has been removed as we now work directly with routes */ /** * Stop the proxy server */ public async stop() { logger.log('info', 'SmartProxy shutting down...'); this.isShuttingDown = true; this.portManager.setShuttingDown(true); // Stop certificate manager if (this.certManager) { await this.certManager.stop(); logger.log('info', 'Certificate manager stopped'); } // Stop NFTablesManager await this.nftablesManager.stop(); logger.log('info', 'NFTablesManager stopped'); // Stop the connection logger if (this.connectionLogger) { clearInterval(this.connectionLogger); this.connectionLogger = null; } // Stop all port listeners await this.portManager.closeAll(); logger.log('info', 'All servers closed. Cleaning up active connections...'); // Clean up all active connections await this.connectionManager.clearConnections(); // Stop HttpProxy await this.httpProxyBridge.stop(); // Clear ACME state manager this.acmeStateManager.clear(); // Stop metrics collector this.metricsCollector.stop(); // Clean up ProtocolDetector singleton const detection = await import('../../detection/index.js'); detection.ProtocolDetector.destroy(); // Flush any pending deduplicated logs connectionLogDeduplicator.flushAll(); logger.log('info', 'SmartProxy shutdown complete.'); } /** * Updates the domain configurations for the proxy * * Note: This legacy method has been removed. Use updateRoutes instead. */ public async updateDomainConfigs(): Promise { logger.log('warn', 'Method updateDomainConfigs() is deprecated. Use updateRoutes() instead.'); throw new Error('updateDomainConfigs() is deprecated - use updateRoutes() instead'); } /** * Verify the challenge route has been properly removed from routes */ private async verifyChallengeRouteRemoved(): Promise { const maxRetries = 10; const retryDelay = 100; // milliseconds for (let i = 0; i < maxRetries; i++) { // Check if the challenge route is still in the active routes const challengeRouteExists = this.settings.routes.some(r => r.name === 'acme-challenge'); if (!challengeRouteExists) { try { logger.log('info', 'Challenge route successfully removed from routes'); } catch (error) { // Silently handle logging errors console.log('[INFO] Challenge route successfully removed from routes'); } return; } // Wait before retrying await plugins.smartdelay.delayFor(retryDelay); } const error = `Failed to verify challenge route removal after ${maxRetries} attempts`; try { logger.log('error', error); } catch (logError) { // Silently handle logging errors console.log(`[ERROR] ${error}`); } throw new Error(error); } /** * Update routes with new configuration * * This method replaces the current route configuration with the provided routes. * It also provisions certificates for routes that require TLS termination and have * `certificate: 'auto'` set in their TLS configuration. * * @param newRoutes Array of route configurations to use * * Example: * ```ts * proxy.updateRoutes([ * { * match: { ports: 443, domains: 'secure.example.com' }, * action: { * type: 'forward', * target: { host: '10.0.0.1', port: 8443 }, * tls: { mode: 'terminate', certificate: 'auto' } * } * } * ]); * ``` */ public async updateRoutes(newRoutes: IRouteConfig[]): Promise { return this.routeUpdateLock.runExclusive(async () => { try { logger.log('info', `Updating routes (${newRoutes.length} routes)`, { routeCount: newRoutes.length, component: 'smart-proxy' }); } catch (error) { // Silently handle logging errors console.log(`[INFO] Updating routes (${newRoutes.length} routes)`); } // Update route orchestrator dependencies if cert manager changed if (this.certManager && !this.routeOrchestrator.getCertManager()) { this.routeOrchestrator.setCertManager(this.certManager); } // Delegate the complex route update logic to RouteOrchestrator const updateResult = await this.routeOrchestrator.updateRoutes( this.settings.routes, newRoutes, { acmePort: this.settings.acme?.port || 80, acmeOptions: this.certManager?.getAcmeOptions(), acmeState: this.certManager?.getState(), globalChallengeRouteActive: this.globalChallengeRouteActive, createCertificateManager: this.createCertificateManager.bind(this), verifyChallengeRouteRemoved: this.verifyChallengeRouteRemoved.bind(this) } ); // Update settings with the new routes this.settings.routes = newRoutes; // Update global state from orchestrator results this.globalChallengeRouteActive = updateResult.newChallengeRouteActive; // Update port usage map from orchestrator this.portUsageMap = updateResult.portUsageMap; // If certificate manager was recreated, update our reference if (updateResult.newCertManager) { this.certManager = updateResult.newCertManager; // Update the orchestrator's reference too this.routeOrchestrator.setCertManager(this.certManager); } }); } /** * Manually provision a certificate for a route */ public async provisionCertificate(routeName: string): Promise { if (!this.certManager) { throw new Error('Certificate manager not initialized'); } const route = this.settings.routes.find(r => r.name === routeName); if (!route) { throw new Error(`Route ${routeName} not found`); } await this.certManager.provisionCertificate(route); } // Port usage tracking methods moved to RouteOrchestrator /** * Force renewal of a certificate */ public async renewCertificate(routeName: string): Promise { if (!this.certManager) { throw new Error('Certificate manager not initialized'); } await this.certManager.renewCertificate(routeName); } /** * Get certificate status for a route */ public getCertificateStatus(routeName: string): ICertStatus | undefined { if (!this.certManager) { return undefined; } return this.certManager.getCertificateStatus(routeName); } /** * Get proxy metrics with clean API * * @returns IMetrics interface with grouped metrics methods */ public getMetrics(): IMetrics { return this.metricsCollector; } /** * Validates if a domain name is valid for certificate issuance */ private isValidDomain(domain: string): boolean { // Very basic domain validation if (!domain || domain.length === 0) { return false; } // Check for wildcard domains (they can't get ACME certs) if (domain.includes('*')) { logger.log('warn', `Wildcard domains like "${domain}" are not supported for automatic ACME certificates`, { domain, component: 'certificate-manager' }); return false; } // Check if domain has at least one dot and no invalid characters const validDomainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; if (!validDomainRegex.test(domain)) { logger.log('warn', `Domain "${domain}" has invalid format for certificate issuance`, { domain, component: 'certificate-manager' }); return false; } return true; } /** * Add a new listening port without changing the route configuration * * This allows you to add a port listener without updating routes. * Useful for preparing to listen on a port before adding routes for it. * * @param port The port to start listening on * @returns Promise that resolves when the port is listening */ public async addListeningPort(port: number): Promise { return this.portManager.addPort(port); } /** * Stop listening on a specific port without changing the route configuration * * This allows you to stop a port listener without updating routes. * Useful for temporary maintenance or port changes. * * @param port The port to stop listening on * @returns Promise that resolves when the port is closed */ public async removeListeningPort(port: number): Promise { return this.portManager.removePort(port); } /** * Get a list of all ports currently being listened on * * @returns Array of port numbers */ public getListeningPorts(): number[] { return this.portManager.getListeningPorts(); } /** * Get statistics about current connections */ public getStatistics(): any { const connectionRecords = this.connectionManager.getConnections(); const terminationStats = this.connectionManager.getTerminationStats(); let tlsConnections = 0; let nonTlsConnections = 0; let keepAliveConnections = 0; let httpProxyConnections = 0; // Analyze active connections for (const record of connectionRecords.values()) { if (record.isTLS) tlsConnections++; else nonTlsConnections++; if (record.hasKeepAlive) keepAliveConnections++; if (record.usingNetworkProxy) httpProxyConnections++; } return { activeConnections: connectionRecords.size, tlsConnections, nonTlsConnections, keepAliveConnections, httpProxyConnections, terminationStats, acmeEnabled: !!this.certManager, port80HandlerPort: this.certManager ? 80 : null, routeCount: this.settings.routes.length, activePorts: this.portManager.getListeningPorts().length, listeningPorts: this.portManager.getListeningPorts() }; } /** * Get a list of eligible domains for ACME certificates */ public getEligibleDomainsForCertificates(): string[] { const domains: string[] = []; // Get domains from routes const routes = this.settings.routes || []; for (const route of routes) { if (!route.match.domains) continue; // Skip routes without TLS termination or auto certificates if (route.action.type !== 'forward' || !route.action.tls || route.action.tls.mode === 'passthrough' || route.action.tls.certificate !== 'auto') continue; const routeDomains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; // Skip domains that can't be used with ACME const eligibleDomains = routeDomains.filter(domain => !domain.includes('*') && this.isValidDomain(domain) ); domains.push(...eligibleDomains); } // Legacy mode is no longer supported return domains; } /** * Get NFTables status */ public async getNfTablesStatus(): Promise> { return this.nftablesManager.getStatus(); } /** * Validate ACME configuration */ private validateAcmeConfiguration(): string[] { const warnings: string[] = []; // Check for routes with certificate: 'auto' const autoRoutes = this.settings.routes.filter(r => r.action.tls?.certificate === 'auto' ); if (autoRoutes.length === 0) { return warnings; } // Check if we have ACME email configuration const hasTopLevelEmail = this.settings.acme?.email; const routesWithEmail = autoRoutes.filter(r => r.action.tls?.acme?.email); if (!hasTopLevelEmail && routesWithEmail.length === 0) { warnings.push( 'Routes with certificate: "auto" require ACME email configuration. ' + 'Add email to either top-level "acme" config or individual route\'s "tls.acme" config.' ); } // Check for port 80 availability for challenges if (autoRoutes.length > 0) { const challengePort = this.settings.acme?.port || 80; const portsInUse = this.routeManager.getListeningPorts(); if (!portsInUse.includes(challengePort)) { warnings.push( `Port ${challengePort} is not configured for any routes but is needed for ACME challenges. ` + `Add a route listening on port ${challengePort} or ensure it's accessible for HTTP-01 challenges.` ); } } // Check for mismatched environments if (this.settings.acme?.useProduction) { const stagingRoutes = autoRoutes.filter(r => r.action.tls?.acme?.useProduction === false ); if (stagingRoutes.length > 0) { warnings.push( 'Top-level ACME uses production but some routes use staging. ' + 'Consider aligning environments to avoid certificate issues.' ); } } // Check for wildcard domains with auto certificates for (const route of autoRoutes) { const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; const wildcardDomains = domains.filter(d => d?.includes('*')); if (wildcardDomains.length > 0) { warnings.push( `Route "${route.name}" has wildcard domain(s) ${wildcardDomains.join(', ')} ` + 'with certificate: "auto". Wildcard certificates require DNS-01 challenges, ' + 'which are not currently supported. Use static certificates instead.' ); } } return warnings; } }