Files
dcrouter/ts/classes.dcrouter.ts

1499 lines
52 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, IEmailDomainConfig } 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';
// 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<number, number>;
/** Custom port configuration for specific ports */
portSettings?: Record<number, any>;
/** 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: /etc/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 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;
// 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
};
// 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();
// 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();
}
// 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<void> {
logger.log('info', 'Setting up CacheDb...');
const cacheConfig = this.options.cacheConfig || {};
// Initialize CacheDb singleton
this.cacheDb = CacheDb.getInstance({
storagePath: cacheConfig.storagePath || '/etc/dcrouter/tsmdb',
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<void> {
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',
targets: 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...');
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 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<void> {
// 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<void> {
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<void> {
// 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<void> {
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<void> {
// 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<number, number>;
}): Promise<boolean> {
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<void> {
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<void> {
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<void> {
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<Array<{name: string; type: string; value: string; ttl?: number}>> {
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<Array<{name: string; type: string; value: string; ttl?: number}>> {
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<void> {
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<Array<{name: string; type: string; value: string; ttl?: number}>> {
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<void> {
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<string | null> {
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<void> {
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();
}
};
}
/**
* Set up RADIUS server for network authentication
*/
private async setupRadiusServer(): Promise<void> {
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<void> {
// 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;