Files
dcrouter/ts/mail/routing/classes.dns.manager.ts
Philipp Kunz ae73de19b2 fix(dns): update DnsManager to use new DNS configuration properties
The DnsManager was still checking for the old dnsDomain property that was
replaced by dnsNsDomains and dnsScopes in the DNS Architecture Improvements.

Changes:
- Replace dnsDomain checks with dnsNsDomains and dnsScopes validation
- Add check to ensure email domain is included in dnsScopes array
- Update NS delegation check to work with multiple nameservers
- Update error messages to guide users to the new configuration format
2025-05-30 16:26:31 +00:00

563 lines
19 KiB
TypeScript

import * as plugins from '../../plugins.js';
import type { IEmailDomainConfig } from './interfaces.js';
import { logger } from '../../logger.js';
import type { DcRouter } from '../../classes.dcrouter.js';
import type { StorageManager } from '../../storage/index.js';
/**
* DNS validation result
*/
export interface IDnsValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
requiredChanges: string[];
}
/**
* DNS records found for a domain
*/
interface IDnsRecords {
mx?: string[];
spf?: string;
dkim?: string;
dmarc?: string;
ns?: string[];
}
/**
* Manages DNS configuration for email domains
* Handles both validation and creation of DNS records
*/
export class DnsManager {
private dcRouter: DcRouter;
private storageManager: StorageManager;
constructor(dcRouter: DcRouter) {
this.dcRouter = dcRouter;
this.storageManager = dcRouter.storageManager;
}
/**
* Validate all domain configurations
*/
async validateAllDomains(domainConfigs: IEmailDomainConfig[]): Promise<Map<string, IDnsValidationResult>> {
const results = new Map<string, IDnsValidationResult>();
for (const config of domainConfigs) {
const result = await this.validateDomain(config);
results.set(config.domain, result);
}
return results;
}
/**
* Validate a single domain configuration
*/
async validateDomain(config: IEmailDomainConfig): Promise<IDnsValidationResult> {
switch (config.dnsMode) {
case 'forward':
return this.validateForwardMode(config);
case 'internal-dns':
return this.validateInternalDnsMode(config);
case 'external-dns':
return this.validateExternalDnsMode(config);
default:
return {
valid: false,
errors: [`Unknown DNS mode: ${config.dnsMode}`],
warnings: [],
requiredChanges: []
};
}
}
/**
* Validate forward mode configuration
*/
private async validateForwardMode(config: IEmailDomainConfig): Promise<IDnsValidationResult> {
const result: IDnsValidationResult = {
valid: true,
errors: [],
warnings: [],
requiredChanges: []
};
// Forward mode doesn't require DNS validation by default
if (!config.dns?.forward?.skipDnsValidation) {
logger.log('info', `DNS validation skipped for forward mode domain: ${config.domain}`);
}
// DKIM keys are still generated for consistency
result.warnings.push(
`Domain "${config.domain}" uses forward mode. DKIM keys will be generated but signing only happens if email is processed.`
);
return result;
}
/**
* Validate internal DNS mode configuration
*/
private async validateInternalDnsMode(config: IEmailDomainConfig): Promise<IDnsValidationResult> {
const result: IDnsValidationResult = {
valid: true,
errors: [],
warnings: [],
requiredChanges: []
};
// Check if DNS configuration is set up
const dnsNsDomains = this.dcRouter.options?.dnsNsDomains;
const dnsScopes = this.dcRouter.options?.dnsScopes;
if (!dnsNsDomains || dnsNsDomains.length === 0) {
result.valid = false;
result.errors.push(
`Domain "${config.domain}" is configured to use internal DNS, but dnsNsDomains is not set in DcRouter configuration.`
);
console.error(
`❌ ERROR: Domain "${config.domain}" is configured to use internal DNS,\n` +
' but dnsNsDomains is not set in DcRouter configuration.\n' +
' Please configure dnsNsDomains to enable the DNS server.\n' +
' Example: dnsNsDomains: ["ns1.myservice.com", "ns2.myservice.com"]'
);
return result;
}
if (!dnsScopes || dnsScopes.length === 0) {
result.valid = false;
result.errors.push(
`Domain "${config.domain}" is configured to use internal DNS, but dnsScopes is not set in DcRouter configuration.`
);
console.error(
`❌ ERROR: Domain "${config.domain}" is configured to use internal DNS,\n` +
' but dnsScopes is not set in DcRouter configuration.\n' +
' Please configure dnsScopes to define authoritative domains.\n' +
' Example: dnsScopes: ["myservice.com", "mail.myservice.com"]'
);
return result;
}
// Check if the email domain is in dnsScopes
if (!dnsScopes.includes(config.domain)) {
result.valid = false;
result.errors.push(
`Domain "${config.domain}" is configured to use internal DNS, but is not included in dnsScopes.`
);
console.error(
`❌ ERROR: Domain "${config.domain}" is configured to use internal DNS,\n` +
` but is not included in dnsScopes: [${dnsScopes.join(', ')}].\n` +
' Please add this domain to dnsScopes to enable internal DNS.\n' +
` Example: dnsScopes: [..., "${config.domain}"]`
);
return result;
}
const primaryNameserver = dnsNsDomains[0];
// Check NS delegation
try {
const nsRecords = await this.resolveNs(config.domain);
const delegatedNameservers = dnsNsDomains.filter(ns => nsRecords.includes(ns));
const isDelegated = delegatedNameservers.length > 0;
if (!isDelegated) {
result.warnings.push(
`NS delegation not found for ${config.domain}. Please add NS records at your registrar.`
);
dnsNsDomains.forEach(ns => {
result.requiredChanges.push(
`Add NS record: ${config.domain}. NS ${ns}.`
);
});
console.log(
`📋 DNS Delegation Required for ${config.domain}:\n` +
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
'Please add these NS records at your domain registrar:\n' +
dnsNsDomains.map(ns => ` ${config.domain}. NS ${ns}.`).join('\n') + '\n' +
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
'This delegation is required for internal DNS mode to work.'
);
} else {
console.log(
`✅ NS delegation verified: ${config.domain} -> [${delegatedNameservers.join(', ')}]`
);
}
} catch (error) {
result.warnings.push(
`Could not verify NS delegation for ${config.domain}: ${error.message}`
);
}
return result;
}
/**
* Validate external DNS mode configuration
*/
private async validateExternalDnsMode(config: IEmailDomainConfig): Promise<IDnsValidationResult> {
const result: IDnsValidationResult = {
valid: true,
errors: [],
warnings: [],
requiredChanges: []
};
try {
// Get current DNS records
const records = await this.checkDnsRecords(config);
const requiredRecords = config.dns?.external?.requiredRecords || ['MX', 'SPF', 'DKIM', 'DMARC'];
// Check MX record
if (requiredRecords.includes('MX') && !records.mx?.length) {
result.requiredChanges.push(
`Add MX record: ${this.getBaseDomain(config.domain)} -> ${config.domain} (priority 10)`
);
}
// Check SPF record
if (requiredRecords.includes('SPF') && !records.spf) {
result.requiredChanges.push(
`Add TXT record: ${this.getBaseDomain(config.domain)} -> "v=spf1 a mx ~all"`
);
}
// Check DKIM record
if (requiredRecords.includes('DKIM') && !records.dkim) {
const selector = config.dkim?.selector || 'default';
const dkimPublicKey = await this.storageManager.get(`/email/dkim/${config.domain}/public.key`);
if (dkimPublicKey) {
const publicKeyBase64 = dkimPublicKey
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
.replace(/-----END PUBLIC KEY-----/g, '')
.replace(/\s/g, '');
result.requiredChanges.push(
`Add TXT record: ${selector}._domainkey.${config.domain} -> "v=DKIM1; k=rsa; p=${publicKeyBase64}"`
);
} else {
result.warnings.push(
`DKIM public key not found for ${config.domain}. It will be generated on first use.`
);
}
}
// Check DMARC record
if (requiredRecords.includes('DMARC') && !records.dmarc) {
result.requiredChanges.push(
`Add TXT record: _dmarc.${this.getBaseDomain(config.domain)} -> "v=DMARC1; p=none; rua=mailto:dmarc@${config.domain}"`
);
}
// Show setup instructions if needed
if (result.requiredChanges.length > 0) {
console.log(
`📋 DNS Configuration Required for ${config.domain}:\n` +
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
result.requiredChanges.map((change, i) => `${i + 1}. ${change}`).join('\n') +
'\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
);
}
} catch (error) {
result.errors.push(`DNS validation failed: ${error.message}`);
result.valid = false;
}
return result;
}
/**
* Check DNS records for a domain
*/
private async checkDnsRecords(config: IEmailDomainConfig): Promise<IDnsRecords> {
const records: IDnsRecords = {};
const baseDomain = this.getBaseDomain(config.domain);
const selector = config.dkim?.selector || 'default';
// Use custom DNS servers if specified
const resolver = new plugins.dns.promises.Resolver();
if (config.dns?.external?.servers?.length) {
resolver.setServers(config.dns.external.servers);
}
// Check MX records
try {
const mxRecords = await resolver.resolveMx(baseDomain);
records.mx = mxRecords.map(mx => mx.exchange);
} catch (error) {
logger.log('debug', `No MX records found for ${baseDomain}`);
}
// Check SPF record
try {
const txtRecords = await resolver.resolveTxt(baseDomain);
const spfRecord = txtRecords.find(records =>
records.some(record => record.startsWith('v=spf1'))
);
if (spfRecord) {
records.spf = spfRecord.join('');
}
} catch (error) {
logger.log('debug', `No SPF record found for ${baseDomain}`);
}
// Check DKIM record
try {
const dkimRecords = await resolver.resolveTxt(`${selector}._domainkey.${config.domain}`);
const dkimRecord = dkimRecords.find(records =>
records.some(record => record.includes('v=DKIM1'))
);
if (dkimRecord) {
records.dkim = dkimRecord.join('');
}
} catch (error) {
logger.log('debug', `No DKIM record found for ${selector}._domainkey.${config.domain}`);
}
// Check DMARC record
try {
const dmarcRecords = await resolver.resolveTxt(`_dmarc.${baseDomain}`);
const dmarcRecord = dmarcRecords.find(records =>
records.some(record => record.startsWith('v=DMARC1'))
);
if (dmarcRecord) {
records.dmarc = dmarcRecord.join('');
}
} catch (error) {
logger.log('debug', `No DMARC record found for _dmarc.${baseDomain}`);
}
return records;
}
/**
* Resolve NS records for a domain
*/
private async resolveNs(domain: string): Promise<string[]> {
try {
const resolver = new plugins.dns.promises.Resolver();
const nsRecords = await resolver.resolveNs(domain);
return nsRecords;
} catch (error) {
logger.log('warn', `Failed to resolve NS records for ${domain}: ${error.message}`);
return [];
}
}
/**
* Get base domain from email domain (e.g., mail.example.com -> example.com)
*/
private getBaseDomain(domain: string): string {
const parts = domain.split('.');
if (parts.length <= 2) {
return domain;
}
// For subdomains like mail.example.com, return example.com
// But preserve domain structure for longer TLDs like .co.uk
if (parts[parts.length - 2].length <= 3 && parts[parts.length - 1].length === 2) {
// Likely a country code TLD like .co.uk
return parts.slice(-3).join('.');
}
return parts.slice(-2).join('.');
}
/**
* Ensure all DNS records are created for configured domains
* This is the main entry point for DNS record management
*/
async ensureDnsRecords(domainConfigs: IEmailDomainConfig[], dkimCreator?: any): Promise<void> {
logger.log('info', `Ensuring DNS records for ${domainConfigs.length} domains`);
// First, validate all domains
const validationResults = await this.validateAllDomains(domainConfigs);
// Then create records for internal-dns domains
const internalDnsDomains = domainConfigs.filter(config => config.dnsMode === 'internal-dns');
if (internalDnsDomains.length > 0) {
await this.createInternalDnsRecords(internalDnsDomains);
// Create DKIM records if DKIMCreator is provided
if (dkimCreator) {
await this.createDkimRecords(domainConfigs, dkimCreator);
}
}
// Log validation results for external-dns domains
for (const [domain, result] of validationResults) {
const config = domainConfigs.find(c => c.domain === domain);
if (config?.dnsMode === 'external-dns' && result.requiredChanges.length > 0) {
logger.log('warn', `External DNS configuration required for ${domain}`);
}
}
}
/**
* Create DNS records for internal-dns mode domains
*/
private async createInternalDnsRecords(domainConfigs: IEmailDomainConfig[]): Promise<void> {
// Check if DNS server is available
if (!this.dcRouter.dnsServer) {
logger.log('warn', 'DNS server not available, skipping internal DNS record creation');
return;
}
logger.log('info', `Creating DNS records for ${domainConfigs.length} internal-dns domains`);
for (const domainConfig of domainConfigs) {
const domain = domainConfig.domain;
const ttl = domainConfig.dns?.internal?.ttl || 3600;
const mxPriority = domainConfig.dns?.internal?.mxPriority || 10;
try {
// 1. Register MX record - points to the email domain itself
this.dcRouter.dnsServer.registerHandler(
domain,
['MX'],
() => ({
name: domain,
type: 'MX',
class: 'IN',
ttl: ttl,
data: {
priority: mxPriority,
exchange: domain
}
})
);
logger.log('info', `MX record registered for ${domain} -> ${domain} (priority ${mxPriority})`);
// Store MX record in StorageManager
await this.storageManager.set(
`/email/dns/${domain}/mx`,
JSON.stringify({
type: 'MX',
priority: mxPriority,
exchange: domain,
ttl: ttl
})
);
// 2. Register SPF record - allows the domain to send emails
const spfRecord = `v=spf1 a mx ~all`;
this.dcRouter.dnsServer.registerHandler(
domain,
['TXT'],
() => ({
name: domain,
type: 'TXT',
class: 'IN',
ttl: ttl,
data: spfRecord
})
);
logger.log('info', `SPF record registered for ${domain}: "${spfRecord}"`);
// Store SPF record in StorageManager
await this.storageManager.set(
`/email/dns/${domain}/spf`,
JSON.stringify({
type: 'TXT',
data: spfRecord,
ttl: ttl
})
);
// 3. Register DMARC record - policy for handling email authentication
const dmarcRecord = `v=DMARC1; p=none; rua=mailto:dmarc@${domain}`;
this.dcRouter.dnsServer.registerHandler(
`_dmarc.${domain}`,
['TXT'],
() => ({
name: `_dmarc.${domain}`,
type: 'TXT',
class: 'IN',
ttl: ttl,
data: dmarcRecord
})
);
logger.log('info', `DMARC record registered for _dmarc.${domain}: "${dmarcRecord}"`);
// Store DMARC record in StorageManager
await this.storageManager.set(
`/email/dns/${domain}/dmarc`,
JSON.stringify({
type: 'TXT',
name: `_dmarc.${domain}`,
data: dmarcRecord,
ttl: ttl
})
);
// Log summary of DNS records created
logger.log('info', `✅ DNS records created for ${domain}:
- MX: ${domain} (priority ${mxPriority})
- SPF: ${spfRecord}
- DMARC: ${dmarcRecord}
- DKIM: Will be created when keys are generated`);
} catch (error) {
logger.log('error', `Failed to create DNS records for ${domain}: ${error.message}`);
}
}
}
/**
* Create DKIM DNS records for all domains
*/
private async createDkimRecords(domainConfigs: IEmailDomainConfig[], dkimCreator: any): Promise<void> {
for (const domainConfig of domainConfigs) {
const domain = domainConfig.domain;
const selector = domainConfig.dkim?.selector || 'default';
try {
// Get DKIM DNS record from DKIMCreator
const dnsRecord = await dkimCreator.getDNSRecordForDomain(domain);
// For internal-dns domains, register the DNS handler
if (domainConfig.dnsMode === 'internal-dns' && this.dcRouter.dnsServer) {
const ttl = domainConfig.dns?.internal?.ttl || 3600;
this.dcRouter.dnsServer.registerHandler(
`${selector}._domainkey.${domain}`,
['TXT'],
() => ({
name: `${selector}._domainkey.${domain}`,
type: 'TXT',
class: 'IN',
ttl: ttl,
data: dnsRecord.value
})
);
logger.log('info', `DKIM DNS record registered for ${selector}._domainkey.${domain}`);
// Store DKIM record in StorageManager
await this.storageManager.set(
`/email/dns/${domain}/dkim`,
JSON.stringify({
type: 'TXT',
name: `${selector}._domainkey.${domain}`,
data: dnsRecord.value,
ttl: ttl
})
);
}
// For external-dns domains, just log what should be configured
if (domainConfig.dnsMode === 'external-dns') {
logger.log('info', `DKIM record for external DNS: ${dnsRecord.name} -> "${dnsRecord.value}"`);
}
} catch (error) {
logger.log('warn', `Could not create DKIM DNS record for ${domain}: ${error.message}`);
}
}
}
}