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 import { UnifiedEmailServer, type IUnifiedEmailServerOptions } from './mail/routing/classes.unified.email.server.js'; import type { IEmailRoute } from './mail/routing/interfaces.js'; import { logger } from './logger.js'; // Import the email configuration helpers directly from mail/delivery import { configureEmailStorage, configureEmailServer } from './mail/delivery/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; }; /** DNS server configuration */ dnsServerConfig?: plugins.smartdns.dnsServerMod.IDnsServerOptions; /** DNS domain for automatic DNS server setup with DoH */ dnsDomain?: string; /** DNS challenge configuration for ACME (optional) */ dnsChallenge?: { /** Cloudflare API key for DNS challenges */ cloudflareApiKey?: string; /** Other DNS providers can be added here */ }; } /** * 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 dnsServer?: plugins.smartdns.dnsServerMod.DnsServer; public emailServer?: UnifiedEmailServer; // Environment access private qenv = new plugins.qenv.Qenv('./', '.nogit/'); constructor(optionsArg: IDcRouterOptions) { // Set defaults in options this.options = { ...optionsArg }; } public async start() { console.log('Starting DcRouter services...'); try { // 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(); // Apply custom email storage configuration if available if (this.emailServer && this.options.emailPortConfig?.receivedEmailsPath) { logger.log('info', 'Applying custom email storage configuration'); configureEmailStorage(this.emailServer, this.options); } } // Set up DNS server if configured by dnsDomain if (this.options.dnsDomain) { await this.setupDnsWithSocketHandler(); } else if (this.options.dnsServerConfig) { // Legacy DNS server setup const { records, ...dnsServerOptions } = this.options.dnsServerConfig as any; this.dnsServer = new plugins.smartdns.dnsServerMod.DnsServer(dnsServerOptions); // Register DNS record handlers if records provided if (records && records.length > 0) { this.registerDnsRecords(records); } await this.dnsServer.start(); console.log(`DNS server started on UDP port ${dnsServerOptions.udpPort || 53}`); } console.log('DcRouter started successfully'); } catch (error) { console.error('Error starting DcRouter:', error); // Try to clean up any services that may have started await this.stop(); throw error; } } /** * 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 domain is configured, add DNS routes if (this.options.dnsDomain) { const dnsRoutes = this.generateDnsRoutes(); console.log(`DNS Routes for domain ${this.options.dnsDomain}:`, 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 }; // If we have DNS challenge handlers, enhance the config if (challengeHandlers.length > 0) { // We'll need to pass this to SmartProxy somehow // For now, we'll set it as a property (smartProxyConfig as any).acmeChallengeHandlers = challengeHandlers; (smartProxyConfig as any).acmeChallengePriority = ['dns-01', 'http-01']; } // 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); }); if (acmeConfig) { this.smartProxy.on('certificate-issued', (event) => { console.log(`[DcRouter] Certificate issued for ${event.domain}, expires ${event.expiryDate}`); }); this.smartProxy.on('certificate-renewed', (event) => { console.log(`[DcRouter] Certificate renewed for ${event.domain}, expires ${event.expiryDate}`); }); this.smartProxy.on('certificate-failed', (event) => { console.error(`[DcRouter] Certificate failed for ${event.domain}:`, event.error); }); } // 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 action based on mode let action: any; if (emailConfig.useSocketHandler) { // Socket-handler mode action = { type: 'socket-handler' as any, socketHandler: this.createMailSocketHandler(port) }; } else { // Traditional forwarding mode const defaultPortMapping = { 25: 10025, // SMTP 587: 10587, // Submission 465: 10465 // SMTPS }; const portMapping = this.options.emailPortConfig?.portMapping || defaultPortMapping; const internalPort = portMapping[port] || port + 10000; action = { type: 'forward', target: { 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); } // Add email domain-based routes if configured if (emailConfig.routes) { for (const route of emailConfig.routes) { emailRoutes.push({ name: route.name, match: { ports: emailConfig.ports, domains: route.match.recipients ? [route.match.recipients.toString().split('@')[1]] : [] }, action: { type: 'forward', target: route.action.type === 'forward' && route.action.forward ? { host: route.action.forward.host, port: route.action.forward.port || 25 } : undefined, tls: { mode: 'passthrough' } } }); } } return emailRoutes; } /** * Generate SmartProxy routes for DNS configuration */ private generateDnsRoutes(): plugins.smartproxy.IRouteConfig[] { if (!this.options.dnsDomain) { return []; } const dnsRoutes: plugins.smartproxy.IRouteConfig[] = []; // Create routes for DNS-over-HTTPS paths const dohPaths = ['/dns-query', '/resolve']; for (const path of dohPaths) { const dohRoute: plugins.smartproxy.IRouteConfig = { name: `dns-over-https-${path.replace('/', '')}`, match: { ports: [443], // HTTPS port for DoH domains: [this.options.dnsDomain], 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 { // Normalize inputs domain = domain.toLowerCase(); pattern = pattern.toLowerCase(); // Check for exact match if (domain === pattern) { return true; } // Check for wildcard match (*.example.com) if (pattern.startsWith('*.')) { const patternSuffix = pattern.slice(2); // Remove the "*." prefix // Check if domain ends with the pattern suffix and has at least one character before it return domain.endsWith(patternSuffix) && domain.length > patternSuffix.length; } // No match return false; } public async stop() { console.log('Stopping DcRouter services...'); try { // Stop all services in parallel for faster shutdown await Promise.all([ // Stop unified email server if running this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', 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() ]); 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 }; // Create config with mapped ports const emailConfig: IUnifiedEmailServerOptions = { ...this.options.emailConfig, 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.updateRoutes(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; } /** * Configure MTA for email handling with custom port and storage settings * @param config Configuration for the MTA service */ public async configureEmailMta(config: { internalPort: number; host?: string; secure?: boolean; storagePath?: string; portMapping?: Record; }): Promise { logger.log('info', 'Configuring MTA service with custom settings'); // Update email port configuration if (!this.options.emailPortConfig) { this.options.emailPortConfig = {}; } // Configure storage paths for received emails if (config.storagePath) { // Set the storage path for received emails this.options.emailPortConfig.receivedEmailsPath = config.storagePath; } // Apply port mapping if provided if (config.portMapping) { this.options.emailPortConfig.portMapping = { ...this.options.emailPortConfig.portMapping, ...config.portMapping }; logger.log('info', `Updated MTA port mappings: ${JSON.stringify(this.options.emailPortConfig.portMapping)}`); } // Use the dedicated helper to configure the email server // Pass through the options specified by the implementation if (this.emailServer) { configureEmailServer(this.emailServer, { ports: [config.internalPort], // Use whatever port the implementation specifies hostname: config.host, tls: config.secure ? { // Basic TLS settings if secure mode is enabled certPath: this.options.tls?.certPath, keyPath: this.options.tls?.keyPath, caPath: this.options.tls?.caPath } : undefined, storagePath: config.storagePath }); } // If email handling is already set up, restart it to apply changes if (this.emailServer) { logger.log('info', 'Restarting unified email handling to apply MTA configuration changes'); await this.stopUnifiedEmailComponents(); await this.setupUnifiedEmailHandling(); } return true; } /** * 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; // Group records by domain pattern const recordsByDomain = new Map(); for (const record of records) { const pattern = record.name.includes('*') ? record.name : `*.${record.name}`; if (!recordsByDomain.has(pattern)) { recordsByDomain.set(pattern, []); } recordsByDomain.get(pattern)!.push(record); } // Register handlers for each domain pattern for (const [domainPattern, domainRecords] of recordsByDomain) { const recordTypes = [...new Set(domainRecords.map(r => r.type))]; this.dnsServer.registerHandler(domainPattern, recordTypes, (question) => { const matchingRecord = domainRecords.find( r => r.name === question.name && r.type === question.type ); if (matchingRecord) { return { name: matchingRecord.name, type: matchingRecord.type, class: 'IN', ttl: matchingRecord.ttl || 300, data: this.parseDnsRecordData(matchingRecord.type, matchingRecord.value) }; } return null; }); } } /** * 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; default: return value; } } /** * Set up DNS server with socket handler for DoH */ private async setupDnsWithSocketHandler(): Promise { if (!this.options.dnsDomain) { throw new Error('dnsDomain is required for DNS socket handler setup'); } logger.log('info', `Setting up DNS server with socket handler for domain: ${this.options.dnsDomain}`); // 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: this.options.dnsDomain, // 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`); } /** * 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(); } }; } /** * Create mail socket handler for email traffic */ private createMailSocketHandler(port: number): (socket: plugins.net.Socket) => Promise { return async (socket: plugins.net.Socket) => { if (!this.emailServer) { logger.log('error', 'Mail socket handler called but email server not initialized'); socket.end(); return; } logger.log('debug', `Mail socket handler: handling connection for port ${port}`); try { // Port 465 requires immediate TLS if (port === 465) { // Wrap the socket in TLS const tlsOptions = { isServer: true, key: this.options.tls?.keyPath ? plugins.fs.readFileSync(this.options.tls.keyPath, 'utf8') : undefined, cert: this.options.tls?.certPath ? plugins.fs.readFileSync(this.options.tls.certPath, 'utf8') : undefined }; const tlsSocket = new plugins.tls.TLSSocket(socket, tlsOptions); tlsSocket.on('secure', () => { // Pass the secure socket to the email server this.emailServer!.handleSocket(tlsSocket, port); }); tlsSocket.on('error', (err) => { logger.log('error', `TLS handshake error on port ${port}: ${err.message}`); socket.destroy(); }); } else { // For ports 25 and 587, pass raw socket (STARTTLS handled by email server) await this.emailServer.handleSocket(socket, port); } } catch (error) { logger.log('error', `Mail socket handler error on port ${port}: ${error.message}`); socket.destroy(); } }; } } // Re-export email server types for convenience export type { IUnifiedEmailServerOptions }; export default DcRouter;