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'; // Import storage manager import { StorageManager, type IStorageConfig } from './storage/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; } /** * 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; public storageManager: StorageManager; // Environment access private qenv = new plugins.qenv.Qenv('./', '.nogit/'); constructor(optionsArg: IDcRouterOptions) { // Set defaults in options this.options = { ...optionsArg }; // Initialize storage manager this.storageManager = new StorageManager(this.options.storage); } 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 with nameservers and scopes if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0 && this.options.dnsScopes && this.options.dnsScopes.length > 0) { await this.setupDnsWithSocketHandler(); } 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 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 }; // 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.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 { // 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; // 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 const dkimCreator = (this.emailServer as any).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; } } /** * 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;