import * as crypto from "crypto"; import * as dns from "dns"; import { promisify } from "util"; import { EntityManager } from "@mikro-orm/core"; import { BadRequestException, Injectable, Logger } from "@nestjs/common"; import { firstValueFrom } from "rxjs"; import { DnsRecordMessage, DomainMessage } from "../../../common/enums/message.enum"; import { MailServerService } from "../../mail-server/services/mail-server.service"; import { CreateDnsRecordDto } from "../DTO/create-dns-record.dto"; import { DnsRecord } from "../entities/dns-record.entity"; import { Domain } from "../entities/domain.entity"; import { DNSRecordType, VerificationStatus } from "../enums/domain-status.enum"; import { DnsRecordRepository } from "../repositories/dns-record.repository"; import { DomainRepository } from "../repositories/domain.repository"; @Injectable() export class DnsService { private readonly logger = new Logger(DnsService.name); private readonly mailServerDomain: string = "mail.danakcorp.com"; private readonly serverDomain: string = "danakcorp.com"; private readonly spfRecord: string = `v=spf1 a mx include:${this.serverDomain} ~all`; // private readonly dmarcRecord: string = `v=DMARC1; p=quarantine; rua=mailto:dmarc@${this.serverDomain}; ruf=mailto:dmarc@${this.serverDomain}`; private readonly resolveTxt = promisify(dns.resolveTxt); private readonly resolveCname = promisify(dns.resolveCname); private readonly resolveMx = promisify(dns.resolveMx); constructor( private readonly dnsRecordRepository: DnsRecordRepository, private readonly domainRepository: DomainRepository, private readonly mailServerService: MailServerService, private readonly em: EntityManager, ) {} /*******************************/ async createDnsRecord(createDnsRecordDto: CreateDnsRecordDto) { const domain = await this.domainRepository.findOne({ id: createDnsRecordDto.domainId }); if (!domain) throw new BadRequestException(DomainMessage.DOMAIN_NOT_FOUND); const dnsRecord = this.dnsRecordRepository.create({ ...createDnsRecordDto, domain, }); await this.em.persistAndFlush(dnsRecord); this.logger.log(`DNS record created: ${dnsRecord.name} ${dnsRecord.type}`); return { dnsRecord }; } /** * Get DNS records for domain */ async getDomainDnsRecords(domainId: string) { const dnsRecords = await this.dnsRecordRepository.findByDomainId(domainId); return { dnsRecords }; } /** * Update DNS record */ async updateDnsRecord(id: string, updates: Partial) { const dnsRecord = await this.dnsRecordRepository.findOne({ id }); if (!dnsRecord) throw new BadRequestException(DnsRecordMessage.DNS_RECORD_NOT_FOUND); this.em.assign(dnsRecord, updates); await this.em.persistAndFlush(dnsRecord); return { dnsRecord }; } /*******************************/ async deleteDnsRecord(id: string) { const dnsRecord = await this.dnsRecordRepository.findOne({ id }); if (!dnsRecord) throw new BadRequestException(DnsRecordMessage.DNS_RECORD_NOT_FOUND); dnsRecord.isActive = false; dnsRecord.deletedAt = new Date(); await this.em.persistAndFlush(dnsRecord); return { message: DnsRecordMessage.DNS_RECORD_DELETED }; } /*******************************/ async generateRequiredDnsRecords(domain: Domain) { const requiredRecords: Partial[] = []; // MX Record requiredRecords.push({ name: "@", fullName: domain.name, type: DNSRecordType.MX, value: this.mailServerDomain, priority: 10, isRequired: true, description: "Mail exchange record for email delivery", }); // SPF Record requiredRecords.push({ name: "@", fullName: domain.name, type: DNSRecordType.TXT, value: this.spfRecord, isRequired: true, description: "Sender Policy Framework record", }); // DMARC Record const dmarcRecord = `v=DMARC1; p=quarantine; rua=mailto:dmarc@${domain.name}; ruf=mailto:dmarc@${domain.name}`; requiredRecords.push({ name: `_dmarc`, fullName: `_dmarc.${domain.name}`, type: DNSRecordType.TXT, value: dmarcRecord, isRequired: true, description: "DMARC policy record", }); const dnsRecords: DnsRecord[] = []; for (const recordData of requiredRecords) { const dnsRecord = this.dnsRecordRepository.create({ name: recordData.name!, fullName: recordData.fullName!, type: recordData.type!, value: recordData.value!, domain, isRequired: recordData.isRequired, description: recordData.description, priority: recordData.priority, ttl: recordData.ttl, }); dnsRecords.push(dnsRecord); } await this.em.persistAndFlush(dnsRecords); this.logger.log(`Generated ${dnsRecords.length} required DNS records for ${domain.name}`); return { dnsRecords }; } /** * Create DKIM record */ async createDKIMRecord(domain: Domain, selector: string, dkimName: string, dkimFullName: string, dkimValue: string, wildduckId: string) { const dkimRecord = this.dnsRecordRepository.create({ name: dkimName, fullName: dkimFullName, type: DNSRecordType.TXT, value: dkimValue, domain, isRequired: true, description: `DKIM public key for selector ${selector}`, wildduckId, }); await this.em.persistAndFlush(dkimRecord); this.logger.log(`DKIM record created for ${domain.name} with selector ${selector}`); return { dkimRecord }; } /** * Generate DKIM keys */ async generateDKIMKeysByMailServer(domainName: string, selector: string) { const data = await firstValueFrom(this.mailServerService.dkim.createDKIMKey({ domain: domainName, selector })); return { publicKey: data.publicKey, privateKey: data.dnsTxt.value, dnsTxt: data.dnsTxt, wildduckId: data.id }; } /** * Generate DKIM keys native */ async generateDKIMKeysByNative() { const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { modulusLength: 2048, publicKeyEncoding: { type: "spki", format: "pem", }, privateKeyEncoding: { type: "pkcs8", format: "pem", }, }); // Extract the public key without headers and newlines for DNS const publicKeyForDNS = publicKey .replace(/-----BEGIN PUBLIC KEY-----/g, "") .replace(/-----END PUBLIC KEY-----/g, "") .replace(/\n/g, ""); return { privateKey, publicKey: publicKeyForDNS, }; } /** * Verify domain DNS configuration */ async verifyDomainDNS(domain: Domain) { const dnsRecords = await this.dnsRecordRepository.findByDomainId(domain.id); const requiredRecords = dnsRecords.filter((record) => record.isRequired && record.isActive); let allVerified = true; for (const record of requiredRecords) { const isVerified = await this.verifyDnsRecord(record); if (!isVerified) { allVerified = false; } } return allVerified; } /** * Verify individual DNS record */ async verifyDnsRecord(dnsRecord: DnsRecord) { try { dnsRecord.verificationAttempts += 1; dnsRecord.lastCheckedAt = new Date(); let isVerified = false; switch (dnsRecord.type) { case DNSRecordType.TXT: case DNSRecordType.SPF: case DNSRecordType.DMARC: case DNSRecordType.DKIM: isVerified = await this.verifyTxtRecord(dnsRecord); break; case DNSRecordType.MX: isVerified = await this.verifyMxRecord(dnsRecord); break; case DNSRecordType.CNAME: isVerified = await this.verifyCnameRecord(dnsRecord); break; default: this.logger.warn(`DNS record type ${dnsRecord.type} verification not implemented`); isVerified = false; } if (isVerified) { dnsRecord.status = VerificationStatus.VERIFIED; dnsRecord.lastVerifiedAt = new Date(); dnsRecord.errorMessage = undefined; } else { dnsRecord.status = VerificationStatus.FAILED; dnsRecord.errorMessage = "DNS record not found or value mismatch"; } await this.em.persistAndFlush(dnsRecord); return isVerified; } catch (error) { dnsRecord.status = VerificationStatus.FAILED; dnsRecord.errorMessage = error instanceof Error ? error.message : "Unknown error"; await this.em.persistAndFlush(dnsRecord); this.logger.error(`DNS verification failed for ${dnsRecord.fullName}:`, error); return false; } } /** * Verify TXT record */ private async verifyTxtRecord(dnsRecord: DnsRecord) { try { const txtRecords = await this.resolveTxt(dnsRecord.fullName); const flatRecords = txtRecords.flat(); // Join all TXT record parts together (handles multi-part TXT records like DKIM) const joinedRecord = flatRecords.join(""); // Check if the expected value matches either the joined record or any individual part return joinedRecord === dnsRecord.value || flatRecords.some((record) => record.includes(dnsRecord.value) || record === dnsRecord.value); } catch (error) { this.logger.debug(`TXT record verification failed for ${dnsRecord.fullName}:`, error instanceof Error ? error.message : "Unknown error"); return false; } } /** * Verify MX record */ private async verifyMxRecord(dnsRecord: DnsRecord) { try { const mxRecords = await this.resolveMx(dnsRecord.fullName); return mxRecords.some((mx) => mx.exchange === dnsRecord.value && mx.priority === (dnsRecord.priority || 10)); } catch (error) { this.logger.debug(`MX record verification failed for ${dnsRecord.name}:`, error instanceof Error ? error.message : "Unknown error"); return false; } } /** * Verify CNAME record */ private async verifyCnameRecord(dnsRecord: DnsRecord) { try { const cnameRecords = await this.resolveCname(dnsRecord.fullName); return cnameRecords.includes(dnsRecord.value); } catch (error) { this.logger.debug(`CNAME record verification failed for ${dnsRecord.name}:`, error instanceof Error ? error.message : "Unknown error"); return false; } } /** * Get DNS record statistics */ async getDnsRecordStats(domainId: string): Promise<{ total: number; verified: number; pending: number; failed: number; required: number; }> { return this.dnsRecordRepository.getDomainDnsStats(domainId); } /** * Verify all DNS records that need checking */ async verifyPendingDnsRecords(): Promise { const records = await this.dnsRecordRepository.findNeedingVerification(); for (const record of records) { try { await this.verifyDnsRecord(record); } catch (error) { this.logger.error(`Failed to verify DNS record ${record.name}:`, error); } } } }