feat(dcrouter): Enhance DcRouter configuration and update documentation
This commit is contained in:
@ -1,23 +1,77 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as paths from '../paths.js';
|
||||
import { SzDcRouterConnector } from './classes.dcr.sz.connector.js';
|
||||
import { SmtpPortConfig, type ISmtpPortSettings } from './classes.smtp.portconfig.js';
|
||||
import { EmailDomainRouter, type IEmailDomainRoutingConfig } from './classes.email.domainrouter.js';
|
||||
|
||||
import type { SzPlatformService } from '../platformservice.js';
|
||||
import { type IMtaConfig, MtaService } from '../mta/classes.mta.js';
|
||||
|
||||
// Certificate types are available via plugins.tsclass
|
||||
|
||||
export interface IDcRouterOptions {
|
||||
platformServiceInstance?: SzPlatformService;
|
||||
/**
|
||||
* Configuration for SMTP forwarding functionality
|
||||
*/
|
||||
export interface ISmtpForwardingConfig {
|
||||
/** Whether SMTP forwarding is enabled */
|
||||
enabled?: boolean;
|
||||
/** SMTP ports to listen on */
|
||||
ports?: number[];
|
||||
/** Default SMTP server hostname */
|
||||
defaultServer: string;
|
||||
/** Default SMTP server port */
|
||||
defaultPort?: number;
|
||||
/** Whether to use TLS when connecting to the default server */
|
||||
useTls?: boolean;
|
||||
/** Preserve source IP address when forwarding */
|
||||
preserveSourceIp?: boolean;
|
||||
/** Domain-specific routing rules */
|
||||
domainRoutes?: Array<{
|
||||
domain: string;
|
||||
server: string;
|
||||
port?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** SmartProxy (TCP/SNI) configuration */
|
||||
smartProxyOptions?: plugins.smartproxy.ISmartProxyOptions;
|
||||
/** Reverse proxy host configurations for HTTP(S) layer */
|
||||
reverseProxyConfigs?: plugins.smartproxy.IReverseProxyConfig[];
|
||||
/** MTA (SMTP) service configuration */
|
||||
/**
|
||||
* Simple domain-based routing configuration
|
||||
*/
|
||||
export interface IDomainRoutingConfig {
|
||||
/** The domain name or pattern (e.g., example.com or *.example.com) */
|
||||
domain: string;
|
||||
/** Target server hostname or IP */
|
||||
targetServer: string;
|
||||
/** Target port */
|
||||
targetPort: number;
|
||||
/** Enable HTTPS/TLS for this route */
|
||||
useTls?: boolean;
|
||||
/** Allow incoming connections from these IP ranges (default: all) */
|
||||
allowedIps?: string[];
|
||||
}
|
||||
|
||||
export interface IDcRouterOptions {
|
||||
/** HTTP/HTTPS domain-based routing */
|
||||
httpDomainRoutes?: IDomainRoutingConfig[];
|
||||
|
||||
/** SMTP forwarding configuration */
|
||||
smtpForwarding?: ISmtpForwardingConfig;
|
||||
|
||||
/** MTA service configuration (if not using SMTP forwarding) */
|
||||
mtaConfig?: IMtaConfig;
|
||||
/** Existing MTA service instance to use instead of creating a new one */
|
||||
|
||||
/** Existing MTA service instance to use (if not using SMTP forwarding) */
|
||||
mtaServiceInstance?: MtaService;
|
||||
|
||||
/** 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;
|
||||
};
|
||||
|
||||
/** DNS server configuration */
|
||||
dnsServerConfig?: plugins.smartdns.IDnsServerOptions;
|
||||
}
|
||||
@ -36,13 +90,17 @@ export interface PortProxyRuleContext {
|
||||
configs: plugins.smartproxy.IPortProxySettings['domainConfigs'];
|
||||
}
|
||||
export class DcRouter {
|
||||
public szDcRouterConnector = new SzDcRouterConnector(this);
|
||||
public options: IDcRouterOptions;
|
||||
|
||||
// Core services
|
||||
public smartProxy?: plugins.smartproxy.SmartProxy;
|
||||
public smtpProxy?: plugins.smartproxy.SmartProxy;
|
||||
public mta?: MtaService;
|
||||
public dnsServer?: plugins.smartdns.DnsServer;
|
||||
/** SMTP rule engine */
|
||||
public smtpRuleEngine?: plugins.smartrule.SmartRule<any>;
|
||||
|
||||
// Environment access
|
||||
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
|
||||
|
||||
constructor(optionsArg: IDcRouterOptions) {
|
||||
// Set defaults in options
|
||||
this.options = {
|
||||
@ -51,193 +109,248 @@ export class DcRouter {
|
||||
}
|
||||
|
||||
public async start() {
|
||||
// Set up MTA service - use existing instance if provided
|
||||
console.log('Starting DcRouter services...');
|
||||
|
||||
try {
|
||||
// 1. Set up HTTP/HTTPS traffic handling with SmartProxy
|
||||
await this.setupHttpProxy();
|
||||
|
||||
// 2. Set up MTA or SMTP forwarding
|
||||
if (this.options.smtpForwarding?.enabled) {
|
||||
await this.setupSmtpForwarding();
|
||||
} else {
|
||||
await this.setupMtaService();
|
||||
}
|
||||
|
||||
// 3. Set up DNS server if configured
|
||||
if (this.options.dnsServerConfig) {
|
||||
this.dnsServer = new plugins.smartdns.DnsServer(this.options.dnsServerConfig);
|
||||
await this.dnsServer.start();
|
||||
console.log('DNS server started');
|
||||
}
|
||||
|
||||
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 for HTTP/HTTPS traffic
|
||||
*/
|
||||
private async setupHttpProxy() {
|
||||
if (!this.options.httpDomainRoutes || this.options.httpDomainRoutes.length === 0) {
|
||||
console.log('No HTTP domain routes configured, skipping HTTP proxy setup');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Setting up SmartProxy for HTTP/HTTPS traffic');
|
||||
|
||||
// Prepare SmartProxy configuration
|
||||
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
|
||||
fromPort: 443,
|
||||
toPort: this.options.httpDomainRoutes[0].targetPort,
|
||||
targetIP: this.options.httpDomainRoutes[0].targetServer,
|
||||
sniEnabled: true,
|
||||
acme: {
|
||||
port: 80,
|
||||
enabled: true,
|
||||
autoRenew: true,
|
||||
useProduction: true,
|
||||
renewThresholdDays: 30,
|
||||
accountEmail: this.options.tls?.contactEmail || 'admin@example.com' // ACME requires an email
|
||||
},
|
||||
globalPortRanges: [{ from: 443, to: 443 }],
|
||||
domainConfigs: []
|
||||
};
|
||||
|
||||
// Create domain configs from the HTTP routes
|
||||
smartProxyConfig.domainConfigs = this.options.httpDomainRoutes.map(route => ({
|
||||
domains: [route.domain],
|
||||
targetIPs: [route.targetServer],
|
||||
allowedIPs: route.allowedIps || ['0.0.0.0/0'],
|
||||
// Skip certificate management for wildcard domains as it's not supported by HTTP-01 challenges
|
||||
certificateManagement: !route.domain.includes('*')
|
||||
}));
|
||||
|
||||
// Create and start the SmartProxy instance
|
||||
this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
|
||||
|
||||
// Listen for certificate events
|
||||
this.smartProxy.on('certificate-issued', event => {
|
||||
console.log(`Certificate issued for ${event.domain}, expires ${event.expiryDate}`);
|
||||
});
|
||||
|
||||
this.smartProxy.on('certificate-renewed', event => {
|
||||
console.log(`Certificate renewed for ${event.domain}, expires ${event.expiryDate}`);
|
||||
});
|
||||
|
||||
await this.smartProxy.start();
|
||||
|
||||
console.log(`HTTP/HTTPS proxy configured with ${smartProxyConfig.domainConfigs.length} domain routes`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the MTA service
|
||||
*/
|
||||
private async setupMtaService() {
|
||||
// Use existing MTA service if provided
|
||||
if (this.options.mtaServiceInstance) {
|
||||
// Use provided MTA service instance
|
||||
this.mta = this.options.mtaServiceInstance;
|
||||
console.log('Using provided MTA service instance');
|
||||
|
||||
// Get the SMTP rule engine from the provided MTA
|
||||
this.smtpRuleEngine = this.mta.smtpRuleEngine;
|
||||
} else if (this.options.mtaConfig) {
|
||||
// Create new MTA service with the provided configuration
|
||||
this.mta = new MtaService(undefined, this.options.mtaConfig);
|
||||
console.log('Created new MTA service instance');
|
||||
|
||||
// Initialize SMTP rule engine
|
||||
this.smtpRuleEngine = this.mta.smtpRuleEngine;
|
||||
}
|
||||
|
||||
// TCP/SNI proxy (SmartProxy)
|
||||
if (this.options.smartProxyOptions) {
|
||||
// Lets setup smartacme
|
||||
let certProvisionFunction: plugins.smartproxy.ISmartProxyOptions['certProvisionFunction'];
|
||||
|
||||
// Check if we can share certificate from MTA service
|
||||
if (this.options.mtaServiceInstance && this.mta) {
|
||||
// Share TLS certificate with MTA service (if available)
|
||||
console.log('Using MTA service certificate for SmartProxy');
|
||||
|
||||
// Create proxy function to get cert from MTA service
|
||||
certProvisionFunction = async (domainArg) => {
|
||||
// Get cert from provided MTA service if available
|
||||
if (this.mta && this.mta.certificate) {
|
||||
console.log(`Using MTA certificate for domain ${domainArg}`);
|
||||
// Return in the format expected by SmartProxy
|
||||
const certExpiry = this.mta.certificate.expiresAt;
|
||||
const certObj: plugins.tsclass.network.ICert = {
|
||||
id: `cert-${domainArg}`,
|
||||
domainName: domainArg,
|
||||
privateKey: this.mta.certificate.privateKey,
|
||||
publicKey: this.mta.certificate.publicKey,
|
||||
created: Date.now(),
|
||||
validUntil: certExpiry instanceof Date ? certExpiry.getTime() : Date.now() + 90 * 24 * 60 * 60 * 1000,
|
||||
csr: ''
|
||||
};
|
||||
return certObj;
|
||||
} else {
|
||||
console.log(`No MTA certificate available for domain ${domainArg}, falling back to ACME`);
|
||||
// Return string literal instead of 'http01' enum value
|
||||
return null; // Let SmartProxy fall back to its default mechanism
|
||||
}
|
||||
};
|
||||
} else if (true) {
|
||||
// Set up ACME for certificate provisioning
|
||||
const smartAcmeInstance = new plugins.smartacme.SmartAcme({
|
||||
accountEmail: this.options.smartProxyOptions.acme.accountEmail,
|
||||
certManager: new plugins.smartacme.certmanagers.MongoCertManager({
|
||||
mongoDbUrl: await this.szDcRouterConnector.getEnvVarOnDemand('MONGO_DB_URL'),
|
||||
mongoDbUser: await this.szDcRouterConnector.getEnvVarOnDemand('MONGO_DB_USER'),
|
||||
mongoDbPass: await this.szDcRouterConnector.getEnvVarOnDemand('MONGO_DB_PASS'),
|
||||
mongoDbName: await this.szDcRouterConnector.getEnvVarOnDemand('MONGO_DB_NAME'),
|
||||
}),
|
||||
environment: 'production',
|
||||
accountPrivateKey: await this.szDcRouterConnector.getEnvVarOnDemand('ACME_ACCOUNT_PRIVATE_KEY'),
|
||||
challengeHandlers: [
|
||||
new plugins.smartacme.handlers.Dns01Handler(new plugins.cloudflare.CloudflareAccount('')) // TODO
|
||||
],
|
||||
});
|
||||
|
||||
certProvisionFunction = async (domainArg) => {
|
||||
try {
|
||||
const domainSupported = await smartAcmeInstance.challengeHandlers[0].checkWetherDomainIsSupported(domainArg);
|
||||
if (!domainSupported) {
|
||||
return null; // Let SmartProxy handle with default mechanism
|
||||
}
|
||||
// Get the certificate and convert to ICert
|
||||
const cert = await smartAcmeInstance.getCertificateForDomain(domainArg);
|
||||
if (typeof cert === 'string') {
|
||||
return null; // String result indicates fallback
|
||||
}
|
||||
|
||||
// Return in the format expected by SmartProxy
|
||||
const result: plugins.tsclass.network.ICert = {
|
||||
id: `cert-${domainArg}`,
|
||||
domainName: domainArg,
|
||||
privateKey: cert.privateKey,
|
||||
publicKey: cert.publicKey,
|
||||
created: Date.now(),
|
||||
validUntil: Date.now() + 90 * 24 * 60 * 60 * 1000, // 90 days
|
||||
csr: ''
|
||||
};
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error(`Certificate error for ${domainArg}:`, err);
|
||||
return null; // Let SmartProxy handle with default mechanism
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Create the SmartProxy instance with the appropriate cert provisioning function
|
||||
const smartProxyOptions = {
|
||||
...this.options.smartProxyOptions,
|
||||
certProvisionFunction
|
||||
};
|
||||
this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyOptions);
|
||||
|
||||
// Configure SmartProxy for SMTP if we have an MTA service
|
||||
if (this.mta) {
|
||||
this.configureSmtpProxy();
|
||||
}
|
||||
}
|
||||
|
||||
// DNS server
|
||||
if (this.options.dnsServerConfig) {
|
||||
this.dnsServer = new plugins.smartdns.DnsServer(this.options.dnsServerConfig);
|
||||
}
|
||||
|
||||
// Start SmartProxy if configured
|
||||
if (this.smartProxy) {
|
||||
await this.smartProxy.start();
|
||||
}
|
||||
|
||||
// Start MTA service if configured and it's our own service (not an external instance)
|
||||
if (this.mta && !this.options.mtaServiceInstance) {
|
||||
// Start the MTA service
|
||||
await this.mta.start();
|
||||
console.log('MTA service started');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up SMTP forwarding with SmartProxy
|
||||
*/
|
||||
private async setupSmtpForwarding() {
|
||||
if (!this.options.smtpForwarding) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start DNS server if configured
|
||||
if (this.dnsServer) {
|
||||
await this.dnsServer.start();
|
||||
}
|
||||
const forwarding = this.options.smtpForwarding;
|
||||
console.log('Setting up SMTP forwarding');
|
||||
|
||||
// Determine which ports to listen on
|
||||
const smtpPorts = forwarding.ports || [25, 587, 465];
|
||||
|
||||
// Create SmartProxy instance for SMTP forwarding
|
||||
this.smtpProxy = new plugins.smartproxy.SmartProxy({
|
||||
// Listen on the first SMTP port
|
||||
fromPort: smtpPorts[0],
|
||||
// Forward to the default server
|
||||
toPort: forwarding.defaultPort || 25,
|
||||
targetIP: forwarding.defaultServer,
|
||||
// Enable SNI if port 465 is included (implicit TLS)
|
||||
sniEnabled: smtpPorts.includes(465),
|
||||
// Preserve source IP if requested
|
||||
preserveSourceIP: forwarding.preserveSourceIp || false,
|
||||
// Create domain configs for SMTP routing
|
||||
domainConfigs: forwarding.domainRoutes?.map(route => ({
|
||||
domains: [route.domain],
|
||||
allowedIPs: ['0.0.0.0/0'], // Allow from anywhere by default
|
||||
targetIPs: [route.server]
|
||||
})) || [],
|
||||
// Include all SMTP ports in the global port ranges
|
||||
globalPortRanges: smtpPorts.map(port => ({ from: port, to: port }))
|
||||
});
|
||||
|
||||
// Start the SMTP proxy
|
||||
await this.smtpProxy.start();
|
||||
|
||||
console.log(`SMTP forwarding configured on ports ${smtpPorts.join(', ')}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure SmartProxy for SMTP ports
|
||||
* 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
|
||||
*/
|
||||
public configureSmtpProxy(): void {
|
||||
if (!this.smartProxy || !this.mta) return;
|
||||
|
||||
const mtaPort = this.mta.config.smtp?.port || 25;
|
||||
try {
|
||||
// Configure SmartProxy to forward SMTP ports to the MTA service
|
||||
const settings = this.smartProxy.settings;
|
||||
// Ensure localhost target for MTA
|
||||
settings.targetIP = settings.targetIP || 'localhost';
|
||||
// Forward all SMTP ports to the MTA port
|
||||
settings.toPort = mtaPort;
|
||||
// Initialize globalPortRanges if needed
|
||||
if (!settings.globalPortRanges) {
|
||||
settings.globalPortRanges = [];
|
||||
}
|
||||
// Add SMTP ports 25, 587, 465 if not already present
|
||||
for (const port of [25, 587, 465]) {
|
||||
if (!settings.globalPortRanges.some((r) => r.from <= port && port <= r.to)) {
|
||||
settings.globalPortRanges.push({ from: port, to: port });
|
||||
}
|
||||
}
|
||||
console.log(`Configured SmartProxy for SMTP ports: 25, 587, 465 → localhost:${mtaPort}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to configure SmartProxy for SMTP:', error);
|
||||
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() {
|
||||
// Stop SmartProxy
|
||||
if (this.smartProxy) {
|
||||
await this.smartProxy.stop();
|
||||
}
|
||||
console.log('Stopping DcRouter services...');
|
||||
|
||||
// Stop MTA service if it's our own (not an external instance)
|
||||
if (this.mta && !this.options.mtaServiceInstance) {
|
||||
await this.mta.stop();
|
||||
}
|
||||
|
||||
// Stop DNS server
|
||||
if (this.dnsServer) {
|
||||
await this.dnsServer.stop();
|
||||
try {
|
||||
// Stop all services in parallel for faster shutdown
|
||||
await Promise.all([
|
||||
// Stop HTTP SmartProxy if running
|
||||
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping HTTP SmartProxy:', err)) : Promise.resolve(),
|
||||
|
||||
// Stop SMTP SmartProxy if running
|
||||
this.smtpProxy ? this.smtpProxy.stop().catch(err => console.error('Error stopping SMTP SmartProxy:', err)) : Promise.resolve(),
|
||||
|
||||
// Stop MTA service if it's our own (not an external instance)
|
||||
(this.mta && !this.options.mtaServiceInstance) ?
|
||||
this.mta.stop().catch(err => console.error('Error stopping MTA service:', 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an SMTP routing rule
|
||||
* Update HTTP domain routes
|
||||
* @param routes New HTTP domain routes
|
||||
*/
|
||||
public addSmtpRule(
|
||||
priority: number,
|
||||
check: (email: any) => Promise<any>,
|
||||
action: (email: any) => Promise<any>
|
||||
): void {
|
||||
this.smtpRuleEngine?.createRule(priority, check, action);
|
||||
public async updateHttpRoutes(routes: IDomainRoutingConfig[]): Promise<void> {
|
||||
this.options.httpDomainRoutes = routes;
|
||||
|
||||
// If SmartProxy is already running, we need to restart it with the new configuration
|
||||
if (this.smartProxy) {
|
||||
// Stop the existing SmartProxy
|
||||
await this.smartProxy.stop();
|
||||
this.smartProxy = undefined;
|
||||
|
||||
// Start a new SmartProxy with the updated configuration
|
||||
await this.setupHttpProxy();
|
||||
}
|
||||
|
||||
console.log(`Updated HTTP routes with ${routes.length} domains`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update SMTP forwarding configuration
|
||||
* @param config New SMTP forwarding configuration
|
||||
*/
|
||||
public async updateSmtpForwarding(config: ISmtpForwardingConfig): Promise<void> {
|
||||
// Stop existing SMTP proxy if running
|
||||
if (this.smtpProxy) {
|
||||
await this.smtpProxy.stop();
|
||||
this.smtpProxy = undefined;
|
||||
}
|
||||
|
||||
// Update configuration
|
||||
this.options.smtpForwarding = config;
|
||||
|
||||
// Restart SMTP forwarding if enabled
|
||||
if (config.enabled) {
|
||||
await this.setupSmtpForwarding();
|
||||
}
|
||||
|
||||
console.log('SMTP forwarding configuration updated');
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user