UNPKG

@push.rocks/smartdns

Version:

A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.

834 lines 72.9 kB
import * as plugins from './plugins.js'; import { DnsSec } from './classes.dnssec.js'; import * as dnsPacket from 'dns-packet'; export class DnsServer { constructor(options) { this.options = options; this.handlers = []; // Track if servers are initialized this.udpServerInitialized = false; this.httpsServerInitialized = false; // Initialize DNSSEC this.dnsSec = new DnsSec({ zone: options.dnssecZone, algorithm: 'ECDSA', // You can change this based on your needs keySize: 256, days: 365, }); // Generate DNSKEY and DS records const { dnskeyRecord } = this.dnsSec.getDsAndKeyPair(); // Parse DNSKEY record into dns-packet format this.dnskeyRecord = this.parseDNSKEYRecord(dnskeyRecord); this.keyTag = this.computeKeyTag(this.dnskeyRecord); } /** * Initialize servers without binding to ports * This is called automatically by start() or can be called manually */ initializeServers() { this.initializeUdpServer(); this.initializeHttpsServer(); } /** * Initialize UDP server without binding */ initializeUdpServer() { if (this.udpServerInitialized) { return; } // Create UDP socket without binding const udpInterface = this.options.udpBindInterface || '0.0.0.0'; const socketType = this.isIPv6(udpInterface) ? 'udp6' : 'udp4'; this.udpServer = plugins.dgram.createSocket(socketType); // Set up UDP message handler this.udpServer.on('message', (msg, rinfo) => { this.handleUdpMessage(msg, rinfo); }); this.udpServer.on('error', (err) => { console.error(`UDP Server error:\n${err.stack}`); this.udpServer.close(); }); this.udpServerInitialized = true; } /** * Initialize HTTPS server without binding */ initializeHttpsServer() { if (this.httpsServerInitialized) { return; } // Create HTTPS server without listening this.httpsServer = plugins.https.createServer({ key: this.options.httpsKey, cert: this.options.httpsCert, }, this.handleHttpsRequest.bind(this)); this.httpsServerInitialized = true; } /** * Handle a raw TCP socket for HTTPS/DoH * @param socket The TCP socket to handle */ handleHttpsSocket(socket) { if (!this.httpsServer) { this.initializeHttpsServer(); } // Emit connection event on the HTTPS server this.httpsServer.emit('connection', socket); } /** * Handle a UDP message manually * @param msg The DNS message buffer * @param rinfo Remote address information * @param responseCallback Optional callback to handle the response */ handleUdpMessage(msg, rinfo, responseCallback) { try { const request = dnsPacket.decode(msg); const response = this.processDnsRequest(request); const responseData = dnsPacket.encode(response); if (responseCallback) { // Use custom callback if provided responseCallback(responseData, rinfo); } else if (this.udpServer && !this.options.manualUdpMode) { // Use the internal UDP server to send response this.udpServer.send(responseData, rinfo.port, rinfo.address); } // In manual mode without callback, caller is responsible for sending response } catch (err) { console.error('Error processing UDP DNS request:', err); } } /** * Process a raw DNS packet and return the response * This is useful for custom transport implementations */ processRawDnsPacket(packet) { try { const request = dnsPacket.decode(packet); const response = this.processDnsRequest(request); return dnsPacket.encode(response); } catch (err) { console.error('Error processing raw DNS packet:', err); throw err; } } registerHandler(domainPattern, recordTypes, handler) { this.handlers.push({ domainPattern, recordTypes, handler }); } // Unregister a specific handler unregisterHandler(domainPattern, recordTypes) { const initialLength = this.handlers.length; this.handlers = this.handlers.filter(handler => !(handler.domainPattern === domainPattern && recordTypes.every(type => handler.recordTypes.includes(type)))); return this.handlers.length < initialLength; } /** * Retrieve SSL certificate for specified domains using Let's Encrypt * @param domainNames Array of domain names to include in the certificate * @param options Configuration options for Let's Encrypt * @returns Object containing certificate, private key, and success status */ async retrieveSslCertificate(domainNames, options = {}) { // Default options const opts = { email: options.email || 'admin@example.com', staging: options.staging !== undefined ? options.staging : false, certDir: options.certDir || './certs' }; // Create certificate directory if it doesn't exist if (!plugins.fs.existsSync(opts.certDir)) { plugins.fs.mkdirSync(opts.certDir, { recursive: true }); } // Filter domains this server is authoritative for const authorizedDomains = this.filterAuthorizedDomains(domainNames); if (authorizedDomains.length === 0) { console.error('None of the provided domains are authorized for this DNS server'); return { cert: '', key: '', success: false }; } console.log(`Retrieving SSL certificate for domains: ${authorizedDomains.join(', ')}`); try { // Allow for override in tests // @ts-ignore - acmeClientOverride is added for testing purposes const acmeClient = this.acmeClientOverride || await import('acme-client'); // Generate or load account key const accountKeyPath = plugins.path.join(opts.certDir, 'account.key'); let accountKey; if (plugins.fs.existsSync(accountKeyPath)) { accountKey = plugins.fs.readFileSync(accountKeyPath); } else { // Generate new account key const { privateKey } = plugins.crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); accountKey = Buffer.from(privateKey); plugins.fs.writeFileSync(accountKeyPath, accountKey); } // Initialize ACME client const client = new acmeClient.Client({ directoryUrl: opts.staging ? acmeClient.directory.letsencrypt.staging : acmeClient.directory.letsencrypt.production, accountKey: accountKey }); // Create or update account await client.createAccount({ termsOfServiceAgreed: true, contact: [`mailto:${opts.email}`] }); // Create order for certificate const order = await client.createOrder({ identifiers: authorizedDomains.map(domain => ({ type: 'dns', value: domain })) }); // Get authorizations const authorizations = await client.getAuthorizations(order); // Track handlers to clean up later const challengeHandlers = []; // Process each authorization for (const auth of authorizations) { const domain = auth.identifier.value; // Get DNS challenge const challenge = auth.challenges.find((c) => c.type === 'dns-01'); if (!challenge) { throw new Error(`No DNS-01 challenge found for ${domain}`); } // Get key authorization and DNS record value const keyAuthorization = await client.getChallengeKeyAuthorization(challenge); const recordValue = this.getDnsRecordValueForChallenge(keyAuthorization); // Create challenge domain (where TXT record should be placed) const challengeDomain = `_acme-challenge.${domain}`; console.log(`Setting up TXT record for ${challengeDomain}: ${recordValue}`); // Register handler for the TXT record this.registerHandler(challengeDomain, ['TXT'], (question) => { if (question.name === challengeDomain && question.type === 'TXT') { return { name: question.name, type: 'TXT', class: 'IN', ttl: 300, data: [recordValue] }; } return null; }); // Track the handler for cleanup challengeHandlers.push({ domain, pattern: challengeDomain }); // Wait briefly for DNS propagation await new Promise(resolve => setTimeout(resolve, 2000)); // Complete the challenge await client.completeChallenge(challenge); // Wait for verification await client.waitForValidStatus(challenge); console.log(`Challenge for ${domain} validated successfully!`); } // Generate certificate key const domainKeyPath = plugins.path.join(opts.certDir, `${authorizedDomains[0]}.key`); const { privateKey } = plugins.crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); plugins.fs.writeFileSync(domainKeyPath, privateKey); // Use the forge.createCsr method and handle typing with a more direct approach const csrResult = await acmeClient.forge.createCsr({ commonName: authorizedDomains[0], altNames: authorizedDomains }); // Finalize the order with the CSR await client.finalizeOrder(order, csrResult.csr); // Get certificate const certificate = await client.getCertificate(order); // Save certificate const certPath = plugins.path.join(opts.certDir, `${authorizedDomains[0]}.cert`); plugins.fs.writeFileSync(certPath, certificate); // Update HTTPS server with new certificate this.options.httpsCert = certificate; this.options.httpsKey = privateKey; // Restart HTTPS server with new certificate (only if not in manual HTTPS mode) if (!this.options.manualHttpsMode) { await this.restartHttpsServer(); } // Clean up challenge handlers for (const handler of challengeHandlers) { this.unregisterHandler(handler.pattern, ['TXT']); console.log(`Cleaned up challenge handler for ${handler.domain}`); } return { cert: certificate, key: privateKey, success: true }; } catch (error) { console.error('Error retrieving SSL certificate:', error); return { cert: '', key: '', success: false }; } } /** * Create DNS record value for the ACME challenge */ getDnsRecordValueForChallenge(keyAuthorization) { // Create SHA-256 digest of the key authorization const digest = plugins.crypto .createHash('sha256') .update(keyAuthorization) .digest('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); return digest; } /** * Restart the HTTPS server with the new certificate */ async restartHttpsServer() { return new Promise((resolve, reject) => { // First check if the server exists if (!this.httpsServer) { console.log('No HTTPS server to restart'); resolve(); return; } this.httpsServer.close(() => { try { // Validate certificate and key before trying to create the server if (!this.options.httpsCert || !this.options.httpsKey) { throw new Error('Missing certificate or key for HTTPS server'); } // For testing, check if we have a mock certificate if (this.options.httpsCert.includes('MOCK_CERTIFICATE')) { console.log('Using mock certificate in test mode'); // In test mode with mock cert, we can use the original cert // @ts-ignore - accessing acmeClientOverride for testing if (this.acmeClientOverride) { this.httpsServer = plugins.https.createServer({ key: this.options.httpsKey, cert: this.options.httpsCert, }, this.handleHttpsRequest.bind(this)); if (!this.options.manualHttpsMode) { const httpsInterface = this.options.httpsBindInterface || '0.0.0.0'; this.httpsServer.listen(this.options.httpsPort, httpsInterface, () => { console.log(`HTTPS DNS server restarted on ${httpsInterface}:${this.options.httpsPort} with test certificate`); resolve(); }); } else { resolve(); } return; } } // Create the new server with the updated certificate this.httpsServer = plugins.https.createServer({ key: this.options.httpsKey, cert: this.options.httpsCert, }, this.handleHttpsRequest.bind(this)); if (!this.options.manualHttpsMode) { const httpsInterface = this.options.httpsBindInterface || '0.0.0.0'; this.httpsServer.listen(this.options.httpsPort, httpsInterface, () => { console.log(`HTTPS DNS server restarted on ${httpsInterface}:${this.options.httpsPort} with new certificate`); resolve(); }); } else { resolve(); } } catch (err) { console.error('Error creating HTTPS server with new certificate:', err); reject(err); } }); }); } /** * Filter domains to include only those the server is authoritative for */ filterAuthorizedDomains(domainNames) { const authorizedDomains = []; for (const domain of domainNames) { // Handle wildcards (*.example.com) if (domain.startsWith('*.')) { const baseDomain = domain.substring(2); if (this.isAuthorizedForDomain(baseDomain)) { authorizedDomains.push(domain); } } // Regular domains else if (this.isAuthorizedForDomain(domain)) { authorizedDomains.push(domain); } } return authorizedDomains; } /** * Validate if a string is a valid IP address (IPv4 or IPv6) */ isValidIpAddress(ip) { // IPv4 pattern const ipv4Pattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; // IPv6 pattern (simplified but more comprehensive) const ipv6Pattern = /^(::1|::)$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/; return ipv4Pattern.test(ip) || ipv6Pattern.test(ip); } /** * Determine if an IP address is IPv6 */ isIPv6(ip) { return ip.includes(':'); } /** * Check if the server is authoritative for a domain */ isAuthorizedForDomain(domain) { // Check if any handler matches this domain for (const handler of this.handlers) { if (plugins.minimatch.minimatch(domain, handler.domainPattern)) { return true; } } // Also check if the domain is the DNSSEC zone itself if (domain === this.options.dnssecZone || domain.endsWith(`.${this.options.dnssecZone}`)) { return true; } return false; } processDnsRequest(request) { const response = { type: 'response', id: request.id, flags: dnsPacket.AUTHORITATIVE_ANSWER | dnsPacket.RECURSION_AVAILABLE | (request.flags & dnsPacket.RECURSION_DESIRED ? dnsPacket.RECURSION_DESIRED : 0), questions: request.questions, answers: [], additionals: [], }; const dnssecRequested = this.isDnssecRequested(request); // Map to group records by type for proper DNSSEC RRset signing const rrsetMap = new Map(); for (const question of request.questions) { console.log(`Query for ${question.name} of type ${question.type}`); let answered = false; const recordsForQuestion = []; // Handle DNSKEY queries if DNSSEC is requested if (dnssecRequested && question.type === 'DNSKEY' && question.name === this.options.dnssecZone) { const dnskeyAnswer = { name: question.name, type: 'DNSKEY', class: 'IN', ttl: 3600, data: this.dnskeyRecord, }; recordsForQuestion.push(dnskeyAnswer); answered = true; } else { // Collect all matching records from handlers for (const handlerEntry of this.handlers) { if (plugins.minimatch.minimatch(question.name, handlerEntry.domainPattern) && handlerEntry.recordTypes.includes(question.type)) { const answer = handlerEntry.handler(question); if (answer) { // Ensure the answer has ttl and class const dnsAnswer = { ...answer, ttl: answer.ttl || 300, class: answer.class || 'IN', }; recordsForQuestion.push(dnsAnswer); answered = true; // Continue processing other handlers to allow multiple records } } } } // Add records to response and group by type for DNSSEC if (recordsForQuestion.length > 0) { for (const record of recordsForQuestion) { response.answers.push(record); } // Group records by type for DNSSEC signing if (dnssecRequested) { const rrsetKey = `${question.name}:${question.type}`; rrsetMap.set(rrsetKey, recordsForQuestion); } } if (!answered) { console.log(`No handler found for ${question.name} of type ${question.type}`); response.flags |= dnsPacket.AUTHORITATIVE_ANSWER; const soaAnswer = { name: question.name, type: 'SOA', class: 'IN', ttl: 3600, data: { mname: this.options.primaryNameserver || `ns1.${this.options.dnssecZone}`, rname: `hostmaster.${this.options.dnssecZone}`, serial: Math.floor(Date.now() / 1000), refresh: 3600, retry: 600, expire: 604800, minimum: 86400, }, }; response.answers.push(soaAnswer); // Add SOA record to DNSSEC signing map if DNSSEC is requested if (dnssecRequested) { const soaKey = `${question.name}:SOA`; rrsetMap.set(soaKey, [soaAnswer]); } } } // Sign RRsets if DNSSEC is requested if (dnssecRequested) { for (const [key, rrset] of rrsetMap) { const [name, type] = key.split(':'); // Sign the entire RRset together const rrsig = this.generateRRSIG(type, rrset, name); response.answers.push(rrsig); } } return response; } isDnssecRequested(request) { if (!request.additionals) return false; for (const additional of request.additionals) { if (additional.type === 'OPT' && typeof additional.flags === 'number') { // The DO bit is the 15th bit (0x8000) if (additional.flags & 0x8000) { return true; } } } return false; } generateRRSIG(type, rrset, name) { // Prepare RRSIG data const algorithm = this.dnsSec.getAlgorithmNumber(); const keyTag = this.keyTag; const signerName = this.options.dnssecZone.endsWith('.') ? this.options.dnssecZone : `${this.options.dnssecZone}.`; const inception = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago const expiration = inception + 86400; // Valid for 1 day const ttl = rrset[0].ttl || 300; // Serialize the RRset in canonical form const rrsetBuffer = this.serializeRRset(rrset); // Sign the RRset const signature = this.dnsSec.signData(rrsetBuffer); // Ensure all fields are defined if (!signerName || !signature) { console.error('RRSIG generation error - missing fields:', { signerName, signature: signature ? 'present' : 'missing', algorithm, keyTag, type }); } // Construct the RRSIG record const rrsig = { name, type: 'RRSIG', class: 'IN', ttl, data: { typeCovered: type, // dns-packet expects the string type algorithm, labels: name.split('.').filter(l => l.length > 0).length, // Fix label count originalTTL: ttl, expiration, inception, keyTag, signersName: signerName || this.options.dnssecZone, // Note: signersName with 's' signature: signature || Buffer.alloc(0), // Fallback to empty buffer }, }; return rrsig; } serializeRRset(rrset) { // Implement canonical DNS RRset serialization as per RFC 4034 Section 6 const buffers = []; for (const rr of rrset) { if (rr.type === 'OPT') { continue; // Skip OPT records } const name = rr.name.endsWith('.') ? rr.name : rr.name + '.'; const nameBuffer = this.nameToBuffer(name.toLowerCase()); const typeValue = this.qtypeToNumber(rr.type); const typeBuffer = Buffer.alloc(2); typeBuffer.writeUInt16BE(typeValue, 0); const classValue = this.classToNumber(rr.class); const classBuffer = Buffer.alloc(2); classBuffer.writeUInt16BE(classValue, 0); const ttlValue = rr.ttl || 300; const ttlBuffer = Buffer.alloc(4); ttlBuffer.writeUInt32BE(ttlValue, 0); // Serialize the data based on the record type const dataBuffer = this.serializeRData(rr.type, rr.data); const rdLengthBuffer = Buffer.alloc(2); rdLengthBuffer.writeUInt16BE(dataBuffer.length, 0); buffers.push(Buffer.concat([nameBuffer, typeBuffer, classBuffer, ttlBuffer, rdLengthBuffer, dataBuffer])); } return Buffer.concat(buffers); } serializeRData(type, data) { // Implement serialization for each record type you support switch (type) { case 'A': return Buffer.from(data.split('.').map((octet) => parseInt(octet, 10))); case 'AAAA': // Handle IPv6 addresses return Buffer.from(data.split(':').flatMap((segment) => { const num = parseInt(segment, 16); return [num >> 8, num & 0xff]; })); case 'TXT': // Handle TXT records for ACME challenges if (Array.isArray(data)) { // Combine all strings and encode as lengths and values const buffers = data.map(str => { const strBuf = Buffer.from(str); const lenBuf = Buffer.alloc(1); lenBuf.writeUInt8(strBuf.length, 0); return Buffer.concat([lenBuf, strBuf]); }); return Buffer.concat(buffers); } return Buffer.alloc(0); case 'DNSKEY': const dnskeyData = data; return Buffer.concat([ Buffer.from([dnskeyData.flags >> 8, dnskeyData.flags & 0xff]), Buffer.from([3]), // Protocol field, always 3 Buffer.from([dnskeyData.algorithm]), dnskeyData.key, ]); case 'NS': // NS records contain domain names return this.nameToBuffer(data); case 'SOA': // Implement SOA record serialization according to RFC 1035 const mname = this.nameToBuffer(data.mname); const rname = this.nameToBuffer(data.rname); const serial = Buffer.alloc(4); serial.writeUInt32BE(data.serial, 0); const refresh = Buffer.alloc(4); refresh.writeUInt32BE(data.refresh, 0); const retry = Buffer.alloc(4); retry.writeUInt32BE(data.retry, 0); const expire = Buffer.alloc(4); expire.writeUInt32BE(data.expire, 0); const minimum = Buffer.alloc(4); minimum.writeUInt32BE(data.minimum, 0); return Buffer.concat([mname, rname, serial, refresh, retry, expire, minimum]); case 'MX': // MX records contain preference (16-bit) and exchange (domain name) const preference = Buffer.alloc(2); preference.writeUInt16BE(data.preference, 0); const exchange = this.nameToBuffer(data.exchange); return Buffer.concat([preference, exchange]); // Add cases for other record types as needed default: throw new Error(`Serialization for record type ${type} is not implemented.`); } } parseDNSKEYRecord(dnskeyRecord) { // Parse the DNSKEY record string into dns-packet format const parts = dnskeyRecord.trim().split(/\s+/); const flags = parseInt(parts[3], 10); const algorithm = parseInt(parts[5], 10); const publicKeyBase64 = parts.slice(6).join(''); const key = Buffer.from(publicKeyBase64, 'base64'); return { flags, algorithm, key, }; } computeKeyTag(dnskeyRecord) { // Compute key tag as per RFC 4034 Appendix B const flags = dnskeyRecord.flags; const algorithm = dnskeyRecord.algorithm; const key = dnskeyRecord.key; const dnskeyRdata = Buffer.concat([ Buffer.from([flags >> 8, flags & 0xff]), Buffer.from([3]), // Protocol field, always 3 Buffer.from([algorithm]), key, ]); let acc = 0; for (let i = 0; i < dnskeyRdata.length; i++) { acc += (i & 1) ? dnskeyRdata[i] : dnskeyRdata[i] << 8; } acc += (acc >> 16) & 0xffff; return acc & 0xffff; } handleHttpsRequest(req, res) { if (req.method === 'POST' && req.url === '/dns-query') { let body = []; req.on('data', (chunk) => { body.push(chunk); }).on('end', () => { const msg = Buffer.concat(body); const request = dnsPacket.decode(msg); const response = this.processDnsRequest(request); const responseData = dnsPacket.encode(response); res.writeHead(200, { 'Content-Type': 'application/dns-message' }); res.end(responseData); }); } else { res.writeHead(404); res.end(); } } async start() { // Initialize servers based on what's needed if (!this.options.manualUdpMode) { this.initializeUdpServer(); } if (!this.options.manualHttpsMode) { this.initializeHttpsServer(); } // Handle different mode combinations const udpManual = this.options.manualUdpMode || false; const httpsManual = this.options.manualHttpsMode || false; if (udpManual && httpsManual) { console.log('DNS server started in full manual mode - ready to accept connections'); return; } else if (udpManual && !httpsManual) { console.log('DNS server started with manual UDP mode and automatic HTTPS binding'); } else if (!udpManual && httpsManual) { console.log('DNS server started with automatic UDP binding and manual HTTPS mode'); } // Validate interface addresses if provided const udpInterface = this.options.udpBindInterface || '0.0.0.0'; const httpsInterface = this.options.httpsBindInterface || '0.0.0.0'; if (this.options.udpBindInterface && !this.isValidIpAddress(this.options.udpBindInterface)) { throw new Error(`Invalid UDP bind interface: ${this.options.udpBindInterface}`); } if (this.options.httpsBindInterface && !this.isValidIpAddress(this.options.httpsBindInterface)) { throw new Error(`Invalid HTTPS bind interface: ${this.options.httpsBindInterface}`); } const promises = []; // Bind UDP if not in manual UDP mode if (!udpManual) { const udpListeningDeferred = plugins.smartpromise.defer(); promises.push(udpListeningDeferred.promise); try { this.udpServer.bind(this.options.udpPort, udpInterface, () => { console.log(`UDP DNS server running on ${udpInterface}:${this.options.udpPort}`); udpListeningDeferred.resolve(); }); } catch (err) { console.error('Error starting UDP DNS server:', err); udpListeningDeferred.reject(err); } } // Bind HTTPS if not in manual HTTPS mode if (!httpsManual) { const httpsListeningDeferred = plugins.smartpromise.defer(); promises.push(httpsListeningDeferred.promise); try { this.httpsServer.listen(this.options.httpsPort, httpsInterface, () => { console.log(`HTTPS DNS server running on ${httpsInterface}:${this.options.httpsPort}`); httpsListeningDeferred.resolve(); }); } catch (err) { console.error('Error starting HTTPS DNS server:', err); httpsListeningDeferred.reject(err); } } if (promises.length > 0) { await Promise.all(promises); } } async stop() { const doneUdp = plugins.smartpromise.defer(); const doneHttps = plugins.smartpromise.defer(); if (this.udpServer) { this.udpServer.close(() => { console.log('UDP DNS server stopped'); if (this.udpServer) { this.udpServer.unref(); this.udpServer = null; } doneUdp.resolve(); }); } else { doneUdp.resolve(); } if (this.httpsServer) { this.httpsServer.close(() => { console.log('HTTPS DNS server stopped'); if (this.httpsServer) { this.httpsServer.unref(); this.httpsServer = null; } doneHttps.resolve(); }); } else { doneHttps.resolve(); } await Promise.all([doneUdp.promise, doneHttps.promise]); this.udpServerInitialized = false; this.httpsServerInitialized = false; } // Helper methods qtypeToNumber(type) { const QTYPE_NUMBERS = { 'A': 1, 'NS': 2, 'CNAME': 5, 'SOA': 6, 'PTR': 12, 'MX': 15, 'TXT': 16, 'AAAA': 28, 'SRV': 33, 'DNSKEY': 48, 'RRSIG': 46, // Add more as needed }; return QTYPE_NUMBERS[type.toUpperCase()] || 0; } classToNumber(cls) { const CLASS_NUMBERS = { 'IN': 1, 'CH': 3, 'HS': 4, // Add more as needed }; if (typeof cls === 'number') { return cls; } return CLASS_NUMBERS[cls.toUpperCase()] || 1; } nameToBuffer(name) { const labels = name.split('.'); const buffers = labels.map(label => { const len = Buffer.byteLength(label, 'utf8'); const buf = Buffer.alloc(1 + len); buf.writeUInt8(len, 0); buf.write(label, 1); return buf; }); return Buffer.concat([...buffers, Buffer.from([0])]); // Add root label } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5kbnNzZXJ2ZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19zZXJ2ZXIvY2xhc3Nlcy5kbnNzZXJ2ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFDeEMsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBQzdDLE9BQU8sS0FBSyxTQUFTLE1BQU0sWUFBWSxDQUFDO0FBNkN4QyxNQUFNLE9BQU8sU0FBUztJQWNwQixZQUFvQixPQUEwQjtRQUExQixZQUFPLEdBQVAsT0FBTyxDQUFtQjtRQVh0QyxhQUFRLEdBQWtCLEVBQUUsQ0FBQztRQU9yQyxtQ0FBbUM7UUFDM0IseUJBQW9CLEdBQVksS0FBSyxDQUFDO1FBQ3RDLDJCQUFzQixHQUFZLEtBQUssQ0FBQztRQUc5QyxvQkFBb0I7UUFDcEIsSUFBSSxDQUFDLE1BQU0sR0FBRyxJQUFJLE1BQU0sQ0FBQztZQUN2QixJQUFJLEVBQUUsT0FBTyxDQUFDLFVBQVU7WUFDeEIsU0FBUyxFQUFFLE9BQU8sRUFBRSwwQ0FBMEM7WUFDOUQsT0FBTyxFQUFFLEdBQUc7WUFDWixJQUFJLEVBQUUsR0FBRztTQUNWLENBQUMsQ0FBQztRQUVILGlDQUFpQztRQUNqQyxNQUFNLEVBQUUsWUFBWSxFQUFFLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxlQUFlLEVBQUUsQ0FBQztRQUV2RCw2Q0FBNkM7UUFDN0MsSUFBSSxDQUFDLFlBQVksR0FBRyxJQUFJLENBQUMsaUJBQWlCLENBQUMsWUFBWSxDQUFDLENBQUM7UUFDekQsSUFBSSxDQUFDLE1BQU0sR0FBRyxJQUFJLENBQUMsYUFBYSxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsQ0FBQztJQUN0RCxDQUFDO0lBRUQ7OztPQUdHO0lBQ0ksaUJBQWlCO1FBQ3RCLElBQUksQ0FBQyxtQkFBbUIsRUFBRSxDQUFDO1FBQzNCLElBQUksQ0FBQyxxQkFBcUIsRUFB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