import * as plugins from './plugins.js'; import * as paths from './paths.js'; // Certificate types are available via plugins.tsclass // Import the email server and its configuration from smartmta import { UnifiedEmailServer, type IUnifiedEmailServerOptions, type IEmailRoute, type IEmailDomainConfig, } from '@push.rocks/smartmta'; import { logger } from './logger.js'; // Import storage manager import { StorageManager, type IStorageConfig } from './storage/index.js'; import { StorageBackedCertManager } from './classes.storage-cert-manager.js'; import { CertProvisionScheduler } from './classes.cert-provision-scheduler.js'; // Import cache system import { CacheDb, CacheCleaner, type ICacheDbOptions } from './cache/index.js'; import { OpsServer } from './opsserver/index.js'; import { MetricsManager } from './monitoring/index.js'; import { RadiusServer, type IRadiusServerConfig } from './radius/index.js'; export interface IDcRouterOptions { /** * Direct SmartProxy configuration - gives full control over HTTP/HTTPS and TCP/SNI traffic * This is the preferred way to configure HTTP/HTTPS and general TCP/SNI traffic */ smartProxyConfig?: plugins.smartproxy.ISmartProxyOptions; /** * Email server configuration * This enables all email handling with pattern-based routing */ emailConfig?: IUnifiedEmailServerOptions; /** * Custom email port configuration * Allows configuring specific ports for email handling * This overrides the default port mapping in the emailConfig */ emailPortConfig?: { /** External to internal port mapping */ portMapping?: Record; /** Custom port configuration for specific ports */ portSettings?: Record; /** Path to store received emails */ receivedEmailsPath?: string; }; /** TLS/certificate configuration */ tls?: { /** Contact email for ACME certificates */ contactEmail: string; /** Domain for main certificate */ domain?: string; /** Path to certificate file (if not using auto-provisioning) */ certPath?: string; /** Path to key file (if not using auto-provisioning) */ keyPath?: string; /** Path to CA certificate file (for custom CAs) */ caPath?: string; }; /** * The nameserver domains (e.g., ['ns1.example.com', 'ns2.example.com']) * These will automatically get A records pointing to publicIp or proxyIps[0] * These are what go in the NS records for ALL domains in dnsScopes */ dnsNsDomains?: string[]; /** * Domains this DNS server is authoritative for (e.g., ['example.com', 'mail.example.org']) * NS records will be auto-generated for these domains * Any DNS record outside these scopes will trigger a warning * Email domains with `internal-dns` mode must be included here */ dnsScopes?: string[]; /** * IPs of proxies that forward traffic to your server (optional) * When defined AND useIngressProxy is true, A records with server IP are replaced with proxy IPs * If not defined or empty, all A records use the real server IP * Helps hide real server IP for security/privacy */ proxyIps?: string[]; /** * Public IP address for nameserver A records (required if proxyIps not set) * This is the IP that will be used for the nameserver domains (dnsNsDomains) * If proxyIps is set, the first proxy IP will be used instead */ publicIp?: string; /** * DNS records to register * Must be within the defined dnsScopes (or receive warning) * Only need A, CNAME, TXT, MX records (NS records auto-generated, SOA handled by smartdns) * Can use `useIngressProxy: false` to expose real server IP (defaults to true) */ dnsRecords?: Array<{ name: string; type: 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SOA'; value: string; ttl?: number; useIngressProxy?: boolean; // Whether to replace server IP with proxy IP (default: true) }>; /** DNS challenge configuration for ACME (optional) */ dnsChallenge?: { /** Cloudflare API key for DNS challenges */ cloudflareApiKey?: string; /** Other DNS providers can be added here */ }; /** Storage configuration */ storage?: IStorageConfig; /** * Cache database configuration using smartdata and LocalTsmDb * Provides persistent caching for emails, IP reputation, bounces, etc. */ cacheConfig?: { /** Enable cache database (default: true) */ enabled?: boolean; /** Storage path for TsmDB data (default: ~/.serve.zone/dcrouter/tsmdb) */ storagePath?: string; /** Database name (default: dcrouter) */ dbName?: string; /** Default TTL in days for cached items (default: 30) */ defaultTTLDays?: number; /** Cleanup interval in hours (default: 1) */ cleanupIntervalHours?: number; /** TTL configuration per data type (in days) */ ttlConfig?: { /** Email cache TTL (default: 30 days) */ emails?: number; /** IP reputation cache TTL (default: 1 day) */ ipReputation?: number; /** Bounce records TTL (default: 30 days) */ bounces?: number; /** DKIM keys TTL (default: 90 days) */ dkimKeys?: number; /** Suppression list TTL (default: 30 days, can be permanent) */ suppression?: number; }; }; /** * RADIUS server configuration for network authentication * Enables MAC Authentication Bypass (MAB) and VLAN assignment */ radiusConfig?: IRadiusServerConfig; } /** * DcRouter can be run on ingress and egress to and from a datacenter site. */ /** * Context passed to HTTP routing rules */ /** * Context passed to port proxy (SmartProxy) routing rules */ export interface PortProxyRuleContext { proxy: plugins.smartproxy.SmartProxy; routes: plugins.smartproxy.IRouteConfig[]; } export class DcRouter { public options: IDcRouterOptions; // Core services public smartProxy?: plugins.smartproxy.SmartProxy; public smartAcme?: plugins.smartacme.SmartAcme; public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer; public emailServer?: UnifiedEmailServer; public radiusServer?: RadiusServer; public storageManager: StorageManager; public opsServer: OpsServer; public metricsManager?: MetricsManager; // Cache system (smartdata + LocalTsmDb) public cacheDb?: CacheDb; public cacheCleaner?: CacheCleaner; // Certificate status tracking from SmartProxy events (keyed by domain) public certificateStatusMap = new Map(); // Certificate provisioning scheduler with per-domain backoff public certProvisionScheduler?: CertProvisionScheduler; // TypedRouter for API endpoints public typedrouter = new plugins.typedrequest.TypedRouter(); // Environment access private qenv = new plugins.qenv.Qenv('./', '.nogit/'); constructor(optionsArg: IDcRouterOptions) { // Set defaults in options this.options = { ...optionsArg }; // Default storage to filesystem if not configured if (!this.options.storage) { this.options.storage = { fsPath: plugins.path.join(paths.dcrouterHomeDir, 'storage'), }; } // Initialize storage manager this.storageManager = new StorageManager(this.options.storage); } public async start() { console.log('╔═══════════════════════════════════════════════════════════════════╗'); console.log('║ Starting DcRouter Services ║'); console.log('╚═══════════════════════════════════════════════════════════════════╝'); this.opsServer = new OpsServer(this); await this.opsServer.start(); try { // Initialize cache database if enabled (default: enabled) if (this.options.cacheConfig?.enabled !== false) { await this.setupCacheDb(); } // Initialize MetricsManager this.metricsManager = new MetricsManager(this); await this.metricsManager.start(); // Set up SmartProxy for HTTP/HTTPS and all traffic including email routes await this.setupSmartProxy(); // Set up unified email handling if configured if (this.options.emailConfig) { await this.setupUnifiedEmailHandling(); } // Set up DNS server if configured with nameservers and scopes if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0 && this.options.dnsScopes && this.options.dnsScopes.length > 0) { await this.setupDnsWithSocketHandler(); } // Set up RADIUS server if configured if (this.options.radiusConfig) { await this.setupRadiusServer(); } this.logStartupSummary(); } catch (error) { console.error('❌ Error starting DcRouter:', error); // Try to clean up any services that may have started await this.stop(); throw error; } } /** * Log comprehensive startup summary */ private logStartupSummary(): void { console.log('\n╔═══════════════════════════════════════════════════════════════════╗'); console.log('║ DcRouter Started Successfully ║'); console.log('╚═══════════════════════════════════════════════════════════════════╝\n'); // Metrics summary if (this.metricsManager) { console.log('📊 Metrics Service:'); console.log(' ├─ SmartMetrics: Active'); console.log(' ├─ SmartProxy Stats: Active'); console.log(' └─ Real-time tracking: Enabled'); } // SmartProxy summary if (this.smartProxy) { console.log('🌐 SmartProxy Service:'); const routeCount = this.options.smartProxyConfig?.routes?.length || 0; console.log(` ├─ Routes configured: ${routeCount}`); console.log(` ├─ ACME enabled: ${this.options.smartProxyConfig?.acme?.enabled || false}`); if (this.options.smartProxyConfig?.acme?.enabled) { console.log(` ├─ ACME email: ${this.options.smartProxyConfig.acme.email || 'not set'}`); console.log(` └─ ACME mode: ${this.options.smartProxyConfig.acme.useProduction ? 'production' : 'staging'}`); } else { console.log(' └─ ACME: disabled'); } } // Email service summary if (this.emailServer && this.options.emailConfig) { console.log('\n📧 Email Service:'); const ports = this.options.emailConfig.ports || []; console.log(` ├─ Ports: ${ports.join(', ')}`); console.log(` ├─ Hostname: ${this.options.emailConfig.hostname || 'localhost'}`); console.log(` ├─ Domains configured: ${this.options.emailConfig.domains?.length || 0}`); if (this.options.emailConfig.domains && this.options.emailConfig.domains.length > 0) { this.options.emailConfig.domains.forEach((domain, index) => { const isLast = index === this.options.emailConfig!.domains!.length - 1; console.log(` ${isLast ? '└─' : '├─'} ${domain.domain} (${domain.dnsMode || 'default'})`); }); } console.log(` └─ DKIM: Initialized for all domains`); } // DNS service summary if (this.dnsServer && this.options.dnsNsDomains && this.options.dnsScopes) { console.log('\n🌍 DNS Service:'); console.log(` ├─ Nameservers: ${this.options.dnsNsDomains.join(', ')}`); console.log(` ├─ Primary NS: ${this.options.dnsNsDomains[0]}`); console.log(` ├─ Authoritative for: ${this.options.dnsScopes.length} domains`); console.log(` ├─ UDP Port: 53`); console.log(` ├─ DNS-over-HTTPS: Enabled via socket handler`); console.log(` └─ DNSSEC: ${this.options.dnsNsDomains[0] ? 'Enabled' : 'Disabled'}`); // Show authoritative domains if (this.options.dnsScopes.length > 0) { console.log('\n Authoritative Domains:'); this.options.dnsScopes.forEach((domain, index) => { const isLast = index === this.options.dnsScopes!.length - 1; console.log(` ${isLast ? '└─' : '├─'} ${domain}`); }); } } // RADIUS service summary if (this.radiusServer && this.options.radiusConfig) { console.log('\n🔐 RADIUS Service:'); console.log(` ├─ Auth Port: ${this.options.radiusConfig.authPort || 1812}`); console.log(` ├─ Acct Port: ${this.options.radiusConfig.acctPort || 1813}`); console.log(` ├─ Clients configured: ${this.options.radiusConfig.clients?.length || 0}`); const vlanStats = this.radiusServer.getVlanManager().getStats(); console.log(` ├─ VLAN mappings: ${vlanStats.totalMappings}`); console.log(` └─ Accounting: ${this.options.radiusConfig.accounting?.enabled ? 'Enabled' : 'Disabled'}`); } // Storage summary if (this.storageManager && this.options.storage) { console.log('\n💾 Storage:'); console.log(` └─ Path: ${this.options.storage.fsPath || 'default'}`); } // Cache database summary if (this.cacheDb) { console.log('\n🗄️ Cache Database (smartdata + LocalTsmDb):'); console.log(` ├─ Storage: ${this.cacheDb.getStoragePath()}`); console.log(` ├─ Database: ${this.cacheDb.getDbName()}`); console.log(` └─ Cleaner: ${this.cacheCleaner?.isActive() ? 'Active' : 'Inactive'} (${(this.options.cacheConfig?.cleanupIntervalHours || 1)}h interval)`); } console.log('\n✅ All services are running\n'); } /** * Set up the cache database (smartdata + LocalTsmDb) */ private async setupCacheDb(): Promise { logger.log('info', 'Setting up CacheDb...'); const cacheConfig = this.options.cacheConfig || {}; // Initialize CacheDb singleton this.cacheDb = CacheDb.getInstance({ storagePath: cacheConfig.storagePath || paths.defaultTsmDbPath, dbName: cacheConfig.dbName || 'dcrouter', debug: false, }); await this.cacheDb.start(); // Start the cache cleaner const cleanupIntervalMs = (cacheConfig.cleanupIntervalHours || 1) * 60 * 60 * 1000; this.cacheCleaner = new CacheCleaner(this.cacheDb, { intervalMs: cleanupIntervalMs, verbose: false, }); this.cacheCleaner.start(); logger.log('info', `CacheDb initialized at ${this.cacheDb.getStoragePath()}`); } /** * Set up SmartProxy with direct configuration and automatic email routes */ private async setupSmartProxy(): Promise { console.log('[DcRouter] Setting up SmartProxy...'); let routes: plugins.smartproxy.IRouteConfig[] = []; let acmeConfig: plugins.smartproxy.IAcmeOptions | undefined; // If user provides full SmartProxy config, use it directly if (this.options.smartProxyConfig) { routes = this.options.smartProxyConfig.routes || []; acmeConfig = this.options.smartProxyConfig.acme; console.log(`[DcRouter] Found ${routes.length} routes in config`); console.log(`[DcRouter] ACME config present: ${!!acmeConfig}`); } // If email config exists, automatically add email routes if (this.options.emailConfig) { const emailRoutes = this.generateEmailRoutes(this.options.emailConfig); console.log(`Email Routes are:`) console.log(emailRoutes) routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy } // If DNS is configured, add DNS routes if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) { const dnsRoutes = this.generateDnsRoutes(); console.log(`DNS Routes for nameservers ${this.options.dnsNsDomains.join(', ')}:`, dnsRoutes); routes = [...routes, ...dnsRoutes]; } // Merge TLS/ACME configuration if provided at root level if (this.options.tls && !acmeConfig) { acmeConfig = { accountEmail: this.options.tls.contactEmail, enabled: true, useProduction: true, autoRenew: true, renewThresholdDays: 30 }; } // Configure DNS challenge if available let challengeHandlers: any[] = []; if (this.options.dnsChallenge?.cloudflareApiKey) { console.log('Configuring Cloudflare DNS challenge for ACME'); const cloudflareAccount = new plugins.cloudflare.CloudflareAccount(this.options.dnsChallenge.cloudflareApiKey); const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(cloudflareAccount); challengeHandlers.push(dns01Handler); } // If we have routes or need a basic SmartProxy instance, create it if (routes.length > 0 || this.options.smartProxyConfig) { console.log('Setting up SmartProxy with combined configuration'); // Create SmartProxy configuration const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = { ...this.options.smartProxyConfig, routes, acme: acmeConfig, certStore: { loadAll: async () => { const keys = await this.storageManager.list('/proxy-certs/'); const certs: Array<{ domain: string; publicKey: string; privateKey: string; ca?: string }> = []; for (const key of keys) { const data = await this.storageManager.getJSON(key); if (data) certs.push(data); } return certs; }, save: async (domain: string, publicKey: string, privateKey: string, ca?: string) => { await this.storageManager.setJSON(`/proxy-certs/${domain}`, { domain, publicKey, privateKey, ca, }); }, remove: async (domain: string) => { await this.storageManager.delete(`/proxy-certs/${domain}`); }, }, }; // Initialize cert provision scheduler this.certProvisionScheduler = new CertProvisionScheduler(this.storageManager); // If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction if (challengeHandlers.length > 0) { this.smartAcme = new plugins.smartacme.SmartAcme({ accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com', certManager: new StorageBackedCertManager(this.storageManager), environment: 'production', challengeHandlers: challengeHandlers, challengePriority: ['dns-01'], }); await this.smartAcme.start(); const scheduler = this.certProvisionScheduler; smartProxyConfig.certProvisionFunction = async (domain, eventComms) => { // Check backoff before attempting provision if (await scheduler.isInBackoff(domain)) { const info = await scheduler.getBackoffInfo(domain); const msg = `Domain ${domain} is in backoff (${info?.failures} failures), retry after ${info?.retryAfter}`; eventComms.warn(msg); throw new Error(msg); } try { // smartacme v9 handles concurrency, per-domain dedup, and rate limiting internally eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`); eventComms.setSource('smartacme-dns-01'); const cert = await this.smartAcme.getCertificateForDomain(domain); if (cert.validUntil) { eventComms.setExpiryDate(new Date(cert.validUntil)); } const result = { id: cert.id, domainName: cert.domainName, created: cert.created, validUntil: cert.validUntil, privateKey: cert.privateKey, publicKey: cert.publicKey, csr: cert.csr, }; // Success — clear any backoff await scheduler.clearBackoff(domain); return result; } catch (err) { // Record failure for backoff tracking await scheduler.recordFailure(domain, err.message); eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${err.message}, falling back to http-01`); return 'http01'; } }; } // Create SmartProxy instance console.log('[DcRouter] Creating SmartProxy instance with config:', JSON.stringify({ routeCount: smartProxyConfig.routes?.length, acmeEnabled: smartProxyConfig.acme?.enabled, acmeEmail: smartProxyConfig.acme?.email, certProvisionFunction: !!smartProxyConfig.certProvisionFunction }, null, 2)); this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig); // Set up event listeners this.smartProxy.on('error', (err) => { console.error('[DcRouter] SmartProxy error:', err); console.error('[DcRouter] Error stack:', err.stack); }); // Always listen for certificate events — emitted by both ACME and certProvisionFunction paths // Events are keyed by domain for domain-centric certificate tracking this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => { console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`); const routeNames = this.findRouteNamesForDomain(event.domain); this.certificateStatusMap.set(event.domain, { status: 'valid', routeNames, expiryDate: event.expiryDate, issuedAt: new Date().toISOString(), source: event.source, }); }); this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => { console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`); const routeNames = this.findRouteNamesForDomain(event.domain); this.certificateStatusMap.set(event.domain, { status: 'valid', routeNames, expiryDate: event.expiryDate, issuedAt: new Date().toISOString(), source: event.source, }); }); this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => { console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error); const routeNames = this.findRouteNamesForDomain(event.domain); this.certificateStatusMap.set(event.domain, { status: 'failed', routeNames, error: event.error, source: event.source, }); }); // Start SmartProxy console.log('[DcRouter] Starting SmartProxy...'); await this.smartProxy.start(); console.log('[DcRouter] SmartProxy started successfully'); console.log(`SmartProxy started with ${routes.length} routes`); } } /** * Generate SmartProxy routes for email configuration */ private generateEmailRoutes(emailConfig: IUnifiedEmailServerOptions): plugins.smartproxy.IRouteConfig[] { const emailRoutes: plugins.smartproxy.IRouteConfig[] = []; // Create routes for each email port for (const port of emailConfig.ports) { // Create a descriptive name for the route based on the port let routeName = 'email-route'; let tlsMode = 'passthrough'; // Handle different email ports differently switch (port) { case 25: // SMTP routeName = 'smtp-route'; tlsMode = 'passthrough'; // STARTTLS handled by email server break; case 587: // Submission routeName = 'submission-route'; tlsMode = 'passthrough'; // STARTTLS handled by email server break; case 465: // SMTPS routeName = 'smtps-route'; tlsMode = 'terminate'; // Terminate TLS and re-encrypt to email server break; default: routeName = `email-port-${port}-route`; tlsMode = 'passthrough'; // Check if we have specific settings for this port if (this.options.emailPortConfig?.portSettings && this.options.emailPortConfig.portSettings[port]) { const portSettings = this.options.emailPortConfig.portSettings[port]; // If this port requires TLS termination, set the mode accordingly if (portSettings.terminateTls) { tlsMode = 'terminate'; } // Override the route name if specified if (portSettings.routeName) { routeName = portSettings.routeName; } } break; } // Create forward action to route to internal email server ports const defaultPortMapping: Record = { 25: 10025, // SMTP 587: 10587, // Submission 465: 10465 // SMTPS }; const portMapping = this.options.emailPortConfig?.portMapping || defaultPortMapping; const internalPort = portMapping[port] || port + 10000; let action: any = { type: 'forward', targets: [{ host: 'localhost', // Forward to internal email server port: internalPort }], tls: { mode: tlsMode as any } }; // For TLS terminate mode, add certificate info if (tlsMode === 'terminate' && action.tls) { action.tls.certificate = 'auto'; } // Create the route configuration const routeConfig: plugins.smartproxy.IRouteConfig = { name: routeName, match: { ports: [port] }, action: action }; // Add the route to our list emailRoutes.push(routeConfig); } return emailRoutes; } /** * Generate SmartProxy routes for DNS configuration */ private generateDnsRoutes(): plugins.smartproxy.IRouteConfig[] { if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) { return []; } const dnsRoutes: plugins.smartproxy.IRouteConfig[] = []; // Create routes for DNS-over-HTTPS paths const dohPaths = ['/dns-query', '/resolve']; // Use the first nameserver domain for DoH routes const primaryNameserver = this.options.dnsNsDomains[0]; for (const path of dohPaths) { const dohRoute: plugins.smartproxy.IRouteConfig = { name: `dns-over-https-${path.replace('/', '')}`, match: { ports: [443], // HTTPS port for DoH domains: [primaryNameserver], path: path }, action: { type: 'socket-handler' as any, socketHandler: this.createDnsSocketHandler() } as any }; dnsRoutes.push(dohRoute); } return dnsRoutes; } /** * Check if a domain matches a pattern (including wildcard support) * @param domain The domain to check * @param pattern The pattern to match against (e.g., "*.example.com") * @returns Whether the domain matches the pattern */ private isDomainMatch(domain: string, pattern: string): boolean { domain = domain.toLowerCase(); pattern = pattern.toLowerCase(); if (domain === pattern) return true; // Routing-glob: *example.com matches example.com, sub.example.com, *.example.com if (pattern.startsWith('*') && !pattern.startsWith('*.')) { const baseDomain = pattern.slice(1); // *nevermind.cloud → nevermind.cloud if (domain === baseDomain || domain === `*.${baseDomain}`) return true; if (domain.endsWith(baseDomain) && domain.length > baseDomain.length) return true; } // Standard wildcard: *.example.com matches sub.example.com and example.com if (pattern.startsWith('*.')) { const suffix = pattern.slice(2); if (domain === suffix) return true; return domain.endsWith(suffix) && domain.length > suffix.length; } return false; } /** * Find the first route name that matches a given domain */ private findRouteNameForDomain(domain: string): string | undefined { if (!this.smartProxy) return undefined; for (const route of this.smartProxy.routeManager.getRoutes()) { if (!route.match.domains || !route.name) continue; const routeDomains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; for (const pattern of routeDomains) { if (this.isDomainMatch(domain, pattern)) return route.name; } } return undefined; } /** * Find ALL route names that match a given domain */ public findRouteNamesForDomain(domain: string): string[] { if (!this.smartProxy) return []; const names: string[] = []; for (const route of this.smartProxy.routeManager.getRoutes()) { if (!route.match.domains || !route.name) continue; const routeDomains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; for (const pattern of routeDomains) { if (this.isDomainMatch(domain, pattern)) { names.push(route.name); break; // This route already matched, no need to check other patterns } } } return names; } public async stop() { console.log('Stopping DcRouter services...'); await this.opsServer.stop(); try { // Stop all services in parallel for faster shutdown await Promise.all([ // Stop cache cleaner if running this.cacheCleaner ? Promise.resolve(this.cacheCleaner.stop()) : Promise.resolve(), // Stop metrics manager if running this.metricsManager ? this.metricsManager.stop().catch(err => console.error('Error stopping MetricsManager:', err)) : Promise.resolve(), // Stop unified email server if running this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', err)) : Promise.resolve(), // Stop SmartAcme if running this.smartAcme ? this.smartAcme.stop().catch(err => console.error('Error stopping SmartAcme:', err)) : Promise.resolve(), // Stop HTTP SmartProxy if running this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(), // Stop DNS server if running this.dnsServer ? this.dnsServer.stop().catch(err => console.error('Error stopping DNS server:', err)) : Promise.resolve(), // Stop RADIUS server if running this.radiusServer ? this.radiusServer.stop().catch(err => console.error('Error stopping RADIUS server:', err)) : Promise.resolve() ]); // Stop cache database after other services (they may need it during shutdown) if (this.cacheDb) { await this.cacheDb.stop().catch(err => console.error('Error stopping CacheDb:', err)); } console.log('All DcRouter services stopped'); } catch (error) { console.error('Error during DcRouter shutdown:', error); throw error; } } /** * Update SmartProxy configuration * @param config New SmartProxy configuration */ public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise { // Stop existing SmartProxy if running if (this.smartProxy) { await this.smartProxy.stop(); this.smartProxy = undefined; } // Update configuration this.options.smartProxyConfig = config; // Start new SmartProxy with updated configuration (will include email routes if configured) await this.setupSmartProxy(); console.log('SmartProxy configuration updated'); } /** * Set up unified email handling with pattern-based routing * This implements the consolidated emailConfig approach */ private async setupUnifiedEmailHandling(): Promise { if (!this.options.emailConfig) { throw new Error('Email configuration is required for unified email handling'); } // Apply port mapping if behind SmartProxy const portMapping = this.options.emailPortConfig?.portMapping || { 25: 10025, // SMTP 587: 10587, // Submission 465: 10465 // SMTPS }; // Transform domains if they are provided as strings let transformedDomains = this.options.emailConfig.domains; if (transformedDomains && transformedDomains.length > 0) { // Check if domains are strings (for backward compatibility) if (typeof transformedDomains[0] === 'string') { transformedDomains = (transformedDomains as any).map((domain: string) => ({ domain, dnsMode: 'external-dns' as const, dkim: { selector: 'default', keySize: 2048, rotateKeys: false, rotationInterval: 90 } })); } } // Create config with mapped ports const emailConfig: IUnifiedEmailServerOptions = { ...this.options.emailConfig, domains: transformedDomains, ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000), hostname: 'localhost' // Listen on localhost for SmartProxy forwarding }; // Create unified email server this.emailServer = new UnifiedEmailServer(this, emailConfig); // Set up error handling this.emailServer.on('error', (err: Error) => { logger.log('error', `UnifiedEmailServer error: ${err.message}`); }); // Start the server await this.emailServer.start(); logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`); } /** * Update the unified email configuration * @param config New email configuration */ public async updateEmailConfig(config: IUnifiedEmailServerOptions): Promise { // Stop existing email components await this.stopUnifiedEmailComponents(); // Update configuration this.options.emailConfig = config; // Start email handling with new configuration await this.setupUnifiedEmailHandling(); console.log('Unified email configuration updated'); } /** * Stop all unified email components */ private async stopUnifiedEmailComponents(): Promise { try { // Stop the unified email server which contains all components if (this.emailServer) { await this.emailServer.stop(); logger.log('info', 'Unified email server stopped'); this.emailServer = undefined; } logger.log('info', 'All unified email components stopped'); } catch (error) { logger.log('error', `Error stopping unified email components: ${error.message}`); throw error; } } /** * Update domain rules for email routing * @param rules New domain rules to apply */ public async updateEmailRoutes(routes: IEmailRoute[]): Promise { // Validate that email config exists if (!this.options.emailConfig) { throw new Error('Email configuration is required before updating routes'); } // Update the configuration this.options.emailConfig.routes = routes; // Update the unified email server if it exists if (this.emailServer) { this.emailServer.updateEmailRoutes(routes); } console.log(`Email routes updated with ${routes.length} routes`); } /** * Get statistics from all components */ public getStats(): any { const stats: any = { emailServer: this.emailServer?.getStats() }; return stats; } /** * Register DNS records with the DNS server * @param records Array of DNS records to register */ private registerDnsRecords(records: Array<{name: string; type: string; value: string; ttl?: number}>): void { if (!this.dnsServer) return; // Register a separate handler for each record // This ensures multiple records of the same type (like NS records) are all served for (const record of records) { // Register handler for this specific record this.dnsServer.registerHandler(record.name, [record.type], (question) => { // Check if this handler matches the question if (question.name === record.name && question.type === record.type) { return { name: record.name, type: record.type, class: 'IN', ttl: record.ttl || 300, data: this.parseDnsRecordData(record.type, record.value) }; } return null; }); } logger.log('info', `Registered ${records.length} DNS handlers (one per record)`); } /** * Parse DNS record data based on record type * @param type DNS record type * @param value DNS record value * @returns Parsed data for the DNS response */ private parseDnsRecordData(type: string, value: string): any { switch (type) { case 'A': return value; // IP address as string case 'MX': const [priority, exchange] = value.split(' '); return { priority: parseInt(priority), exchange }; case 'TXT': return value; case 'NS': return value; case 'SOA': // SOA format: primary-ns admin-email serial refresh retry expire minimum const parts = value.split(' '); return { mname: parts[0], rname: parts[1], serial: parseInt(parts[2]), refresh: parseInt(parts[3]), retry: parseInt(parts[4]), expire: parseInt(parts[5]), minimum: parseInt(parts[6]) }; default: return value; } } /** * Set up DNS server with socket handler for DoH */ private async setupDnsWithSocketHandler(): Promise { if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) { throw new Error('dnsNsDomains is required for DNS server setup'); } if (!this.options.dnsScopes || this.options.dnsScopes.length === 0) { throw new Error('dnsScopes is required for DNS server setup'); } const primaryNameserver = this.options.dnsNsDomains[0]; logger.log('info', `Setting up DNS server with primary nameserver: ${primaryNameserver}`); // Get VM IP address for UDP binding const networkInterfaces = plugins.os.networkInterfaces(); let vmIpAddress = '0.0.0.0'; // Default to all interfaces // Try to find the VM's internal IP address for (const [_name, interfaces] of Object.entries(networkInterfaces)) { if (interfaces) { for (const iface of interfaces) { if (!iface.internal && iface.family === 'IPv4') { vmIpAddress = iface.address; break; } } } } // Create DNS server instance with manual HTTPS mode this.dnsServer = new plugins.smartdns.dnsServerMod.DnsServer({ udpPort: 53, udpBindInterface: vmIpAddress, httpsPort: 443, // Required but won't bind due to manual mode manualHttpsMode: true, // Enable manual HTTPS socket handling dnssecZone: primaryNameserver, primaryNameserver: primaryNameserver, // Automatically generates correct SOA records // For now, use self-signed cert until we integrate with Let's Encrypt httpsKey: '', httpsCert: '' }); // Start the DNS server (UDP only) await this.dnsServer.start(); logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`); // Validate DNS configuration await this.validateDnsConfiguration(); // Generate and register authoritative records const authoritativeRecords = await this.generateAuthoritativeRecords(); // Generate email DNS records const emailDnsRecords = await this.generateEmailDnsRecords(); // Initialize DKIM for all email domains await this.initializeDkimForEmailDomains(); // Load DKIM records from JSON files (they should now exist) const dkimRecords = await this.loadDkimRecords(); // Combine all records: authoritative, email, DKIM, and user-defined const allRecords = [...authoritativeRecords, ...emailDnsRecords, ...dkimRecords]; if (this.options.dnsRecords && this.options.dnsRecords.length > 0) { allRecords.push(...this.options.dnsRecords); } // Apply proxy IP replacement if configured await this.applyProxyIpReplacement(allRecords); // Register all DNS records if (allRecords.length > 0) { this.registerDnsRecords(allRecords); logger.log('info', `Registered ${allRecords.length} DNS records (${authoritativeRecords.length} authoritative, ${emailDnsRecords.length} email, ${dkimRecords.length} DKIM, ${this.options.dnsRecords?.length || 0} user-defined)`); } } /** * Create DNS socket handler for DoH */ private createDnsSocketHandler(): (socket: plugins.net.Socket) => Promise { return async (socket: plugins.net.Socket) => { if (!this.dnsServer) { logger.log('error', 'DNS socket handler called but DNS server not initialized'); socket.end(); return; } logger.log('debug', 'DNS socket handler: passing socket to DnsServer'); try { // Use the built-in socket handler from smartdns // This handles HTTP/2, DoH protocol, etc. await (this.dnsServer as any).handleHttpsSocket(socket); } catch (error) { logger.log('error', `DNS socket handler error: ${error.message}`); socket.destroy(); } }; } /** * Validate DNS configuration */ private async validateDnsConfiguration(): Promise { if (!this.options.dnsNsDomains || !this.options.dnsScopes) { return; } logger.log('info', 'Validating DNS configuration...'); // Check if email domains with internal-dns are in dnsScopes if (this.options.emailConfig?.domains) { for (const domainConfig of this.options.emailConfig.domains) { if (domainConfig.dnsMode === 'internal-dns' && !this.options.dnsScopes.includes(domainConfig.domain)) { logger.log('warn', `Email domain '${domainConfig.domain}' with internal-dns mode is not in dnsScopes. It should be added to dnsScopes.`); } } } // Validate user-provided DNS records are within scopes if (this.options.dnsRecords) { for (const record of this.options.dnsRecords) { const recordDomain = this.extractDomain(record.name); const isInScope = this.options.dnsScopes.some(scope => recordDomain === scope || recordDomain.endsWith(`.${scope}`) ); if (!isInScope) { logger.log('warn', `DNS record for '${record.name}' is outside defined scopes [${this.options.dnsScopes.join(', ')}]`); } } } } /** * Generate email DNS records for domains with internal-dns mode */ private async generateEmailDnsRecords(): Promise> { const records: Array<{name: string; type: string; value: string; ttl?: number}> = []; if (!this.options.emailConfig?.domains) { return records; } // Filter domains with internal-dns mode const internalDnsDomains = this.options.emailConfig.domains.filter( domain => domain.dnsMode === 'internal-dns' ); for (const domainConfig of internalDnsDomains) { const domain = domainConfig.domain; const ttl = domainConfig.dns?.internal?.ttl || 3600; const mxPriority = domainConfig.dns?.internal?.mxPriority || 10; // MX record - points to the domain itself for email handling records.push({ name: domain, type: 'MX', value: `${mxPriority} ${domain}`, ttl }); // SPF record - using sensible defaults const spfRecord = 'v=spf1 a mx ~all'; records.push({ name: domain, type: 'TXT', value: spfRecord, ttl }); // DMARC record - using sensible defaults const dmarcPolicy = 'none'; // Start with 'none' policy for monitoring const dmarcEmail = `dmarc@${domain}`; records.push({ name: `_dmarc.${domain}`, type: 'TXT', value: `v=DMARC1; p=${dmarcPolicy}; rua=mailto:${dmarcEmail}`, ttl }); // Note: DKIM records will be generated later when DKIM keys are available // They require the DKIMCreator which is part of the email server } logger.log('info', `Generated ${records.length} email DNS records for ${internalDnsDomains.length} internal-dns domains`); return records; } /** * Load DKIM records from JSON files * Reads all *.dkimrecord.json files from the DNS records directory */ private async loadDkimRecords(): Promise> { const records: Array<{name: string; type: string; value: string; ttl?: number}> = []; try { // Ensure paths are imported const dnsDir = paths.dnsRecordsDir; // Check if directory exists if (!plugins.fs.existsSync(dnsDir)) { logger.log('debug', 'No DNS records directory found, skipping DKIM record loading'); return records; } // Read all files in the directory const files = plugins.fs.readdirSync(dnsDir); const dkimFiles = files.filter(f => f.endsWith('.dkimrecord.json')); logger.log('info', `Found ${dkimFiles.length} DKIM record files`); // Load each DKIM record for (const file of dkimFiles) { try { const filePath = plugins.path.join(dnsDir, file); const fileContent = plugins.fs.readFileSync(filePath, 'utf8'); const dkimRecord = JSON.parse(fileContent); // Validate record structure if (dkimRecord.name && dkimRecord.type === 'TXT' && dkimRecord.value) { records.push({ name: dkimRecord.name, type: 'TXT', value: dkimRecord.value, ttl: 3600 // Standard DKIM TTL }); logger.log('info', `Loaded DKIM record for ${dkimRecord.name}`); } else { logger.log('warn', `Invalid DKIM record structure in ${file}`); } } catch (error) { logger.log('error', `Failed to load DKIM record from ${file}: ${error.message}`); } } } catch (error) { logger.log('error', `Failed to load DKIM records: ${error.message}`); } return records; } /** * Initialize DKIM keys for all configured email domains * This ensures DKIM records are available immediately at startup */ private async initializeDkimForEmailDomains(): Promise { if (!this.options.emailConfig?.domains || !this.emailServer) { return; } logger.log('info', 'Initializing DKIM keys for email domains...'); // Get DKIMCreator instance from email server (public in smartmta) const dkimCreator = this.emailServer.dkimCreator; if (!dkimCreator) { logger.log('warn', 'DKIMCreator not available, skipping DKIM initialization'); return; } // Ensure necessary directories exist paths.ensureDirectories(); // Generate DKIM keys for each email domain for (const domainConfig of this.options.emailConfig.domains) { try { // Generate DKIM keys for all domains, regardless of DNS mode // This ensures keys are ready even if DNS mode changes later await dkimCreator.handleDKIMKeysForDomain(domainConfig.domain); logger.log('info', `DKIM keys initialized for ${domainConfig.domain}`); } catch (error) { logger.log('error', `Failed to initialize DKIM for ${domainConfig.domain}: ${error.message}`); } } logger.log('info', 'DKIM initialization complete'); } /** * Generate authoritative DNS records (NS only) for all domains in dnsScopes * SOA records are now automatically generated by smartdns with primaryNameserver setting */ private async generateAuthoritativeRecords(): Promise> { const records: Array<{name: string; type: string; value: string; ttl?: number}> = []; if (!this.options.dnsNsDomains || !this.options.dnsScopes) { return records; } // Determine the public IP for nameserver A records let publicIp: string | null = null; // Use proxy IPs if configured (these should be public IPs) if (this.options.proxyIps && this.options.proxyIps.length > 0) { publicIp = this.options.proxyIps[0]; // Use first proxy IP logger.log('info', `Using proxy IP for nameserver A records: ${publicIp}`); } else if (this.options.publicIp) { // Use explicitly configured public IP publicIp = this.options.publicIp; logger.log('info', `Using configured public IP for nameserver A records: ${publicIp}`); } else { // Auto-discover public IP using smartnetwork try { logger.log('info', 'Auto-discovering public IP address...'); const smartNetwork = new plugins.smartnetwork.SmartNetwork(); const publicIps = await smartNetwork.getPublicIps(); if (publicIps.v4) { publicIp = publicIps.v4; logger.log('info', `Auto-discovered public IPv4: ${publicIp}`); } else { logger.log('warn', 'Could not auto-discover public IPv4 address'); } } catch (error) { logger.log('error', `Failed to auto-discover public IP: ${error.message}`); } if (!publicIp) { logger.log('warn', 'No public IP available. Nameserver A records require either proxyIps, publicIp, or successful auto-discovery.'); } } // Generate A records for nameservers if we have a public IP if (publicIp) { for (const nsDomain of this.options.dnsNsDomains) { records.push({ name: nsDomain, type: 'A', value: publicIp, ttl: 3600 }); } logger.log('info', `Generated A records for ${this.options.dnsNsDomains.length} nameservers`); } // Generate NS records for each domain in scopes for (const domain of this.options.dnsScopes) { // Add NS records for all nameservers for (const nsDomain of this.options.dnsNsDomains) { records.push({ name: domain, type: 'NS', value: nsDomain, ttl: 3600 }); } // SOA records are now automatically generated by smartdns DnsServer // with the primaryNameserver configuration option } logger.log('info', `Generated ${records.length} total records (A + NS) for ${this.options.dnsScopes.length} domains`); return records; } /** * Extract the base domain from a DNS record name */ private extractDomain(recordName: string): string { // Handle wildcards if (recordName.startsWith('*.')) { recordName = recordName.substring(2); } return recordName; } /** * Apply proxy IP replacement logic to DNS records */ private async applyProxyIpReplacement(records: Array<{name: string; type: string; value: string; ttl?: number; useIngressProxy?: boolean}>): Promise { if (!this.options.proxyIps || this.options.proxyIps.length === 0) { return; // No proxy IPs configured, skip replacement } // Get server's public IP const serverIp = await this.detectServerPublicIp(); if (!serverIp) { logger.log('warn', 'Could not detect server public IP, skipping proxy IP replacement'); return; } logger.log('info', `Applying proxy IP replacement. Server IP: ${serverIp}, Proxy IPs: ${this.options.proxyIps.join(', ')}`); let proxyIndex = 0; for (const record of records) { if (record.type === 'A' && record.value === serverIp && record.useIngressProxy !== false) { // Round-robin through proxy IPs const proxyIp = this.options.proxyIps[proxyIndex % this.options.proxyIps.length]; logger.log('info', `Replacing A record for ${record.name}: ${record.value} → ${proxyIp}`); record.value = proxyIp; proxyIndex++; } } } /** * Detect the server's public IP address */ private async detectServerPublicIp(): Promise { try { const smartNetwork = new plugins.smartnetwork.SmartNetwork(); const publicIps = await smartNetwork.getPublicIps(); if (publicIps.v4) { return publicIps.v4; } return null; } catch (error) { logger.log('warn', `Failed to detect public IP: ${error.message}`); return null; } } /** * Set up RADIUS server for network authentication */ private async setupRadiusServer(): Promise { if (!this.options.radiusConfig) { return; } logger.log('info', 'Setting up RADIUS server...'); this.radiusServer = new RadiusServer(this.options.radiusConfig, this.storageManager); await this.radiusServer.start(); logger.log('info', `RADIUS server started on ports ${this.options.radiusConfig.authPort || 1812} (auth) and ${this.options.radiusConfig.acctPort || 1813} (acct)`); } /** * Update RADIUS configuration at runtime */ public async updateRadiusConfig(config: IRadiusServerConfig): Promise { // Stop existing RADIUS server if running if (this.radiusServer) { await this.radiusServer.stop(); this.radiusServer = undefined; } // Update configuration this.options.radiusConfig = config; // Start with new configuration await this.setupRadiusServer(); logger.log('info', 'RADIUS configuration updated'); } } // Re-export email server types for convenience export type { IUnifiedEmailServerOptions }; // Re-export RADIUS types for convenience export type { IRadiusServerConfig }; export default DcRouter;