217 lines
7.1 KiB
JavaScript
217 lines
7.1 KiB
JavaScript
const dgram = require('dgram');
|
|
const dnsPacket = require('dns-packet');
|
|
const { logDebug, logError, logWarn, logInfo } = require('../infrastructure/logger');
|
|
|
|
// DNS resolver connection pool
|
|
class DNSPool {
|
|
constructor(maxConnections = 5) {
|
|
this.maxConnections = maxConnections;
|
|
this.resolvers = [];
|
|
this.activeQueries = new Map();
|
|
this.queryId = 0;
|
|
}
|
|
|
|
/**
|
|
* Get or create a DNS resolver socket
|
|
* @returns {dgram.Socket} - DNS resolver socket
|
|
*/
|
|
getResolver() {
|
|
if (this.resolvers.length < this.maxConnections) {
|
|
const resolver = dgram.createSocket('udp4');
|
|
resolver.on('error', (err) => {
|
|
logError('DNSPool', `Resolver error: ${err.message}`);
|
|
});
|
|
this.resolvers.push(resolver);
|
|
return resolver;
|
|
}
|
|
// Round-robin through existing resolvers
|
|
return this.resolvers[this.queryId % this.resolvers.length];
|
|
}
|
|
|
|
/**
|
|
* Get list of DNS servers from environment variable
|
|
* @returns {string[]} - Array of DNS server IP addresses
|
|
*/
|
|
getDnsServers() {
|
|
const publicDnsServerEnv = process.env.PUBLIC_DNS_SERVER || '1.1.1.1';
|
|
return publicDnsServerEnv.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
|
}
|
|
|
|
/**
|
|
* Query public DNS with failover support
|
|
* @param {object} query - DNS query object
|
|
* @returns {Promise<object|null>} - DNS response or null
|
|
*/
|
|
async query(query) {
|
|
const dnsServers = this.getDnsServers();
|
|
|
|
// Try each DNS server in order (failover strategy)
|
|
for (let serverIndex = 0; serverIndex < dnsServers.length; serverIndex++) {
|
|
const publicDNSServer = dnsServers[serverIndex];
|
|
const isLastServer = serverIndex === dnsServers.length - 1;
|
|
|
|
const result = await new Promise((resolve) => {
|
|
const resolver = this.getResolver();
|
|
const queryId = ++this.queryId;
|
|
let resolved = false;
|
|
|
|
const cleanup = () => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
const queryData = this.activeQueries.get(queryId);
|
|
if (queryData) {
|
|
// Remove listeners explicitly
|
|
if (queryData.handler) {
|
|
resolver.removeListener('message', queryData.handler);
|
|
}
|
|
if (queryData.errorHandler) {
|
|
resolver.removeListener('error', queryData.errorHandler);
|
|
}
|
|
if (queryData.timeout) {
|
|
clearTimeout(queryData.timeout);
|
|
}
|
|
this.activeQueries.delete(queryId);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Set timeout
|
|
const timeout = setTimeout(() => {
|
|
logWarn('DNSPool', `DNS query timeout for ${query.questions[0].name} on server ${publicDNSServer}`);
|
|
cleanup();
|
|
resolve(null);
|
|
}, 5000);
|
|
|
|
const handler = (msg) => {
|
|
if (resolved) return;
|
|
clearTimeout(timeout);
|
|
try {
|
|
const response = dnsPacket.decode(msg);
|
|
logDebug('DNSPool', `Public DNS response for ${query.questions[0].name} from ${publicDNSServer}: ${JSON.stringify(response.answers)}`);
|
|
cleanup();
|
|
resolve(response);
|
|
} catch (err) {
|
|
logError('DNSPool', `Error decoding DNS response from ${publicDNSServer}: ${err.message}`);
|
|
cleanup();
|
|
resolve(null);
|
|
}
|
|
};
|
|
|
|
const errorHandler = (err) => {
|
|
if (resolved) return;
|
|
clearTimeout(timeout);
|
|
logWarn('DNSPool', `Error receiving DNS response from ${publicDNSServer}: ${err.message}`);
|
|
cleanup();
|
|
resolve(null);
|
|
};
|
|
|
|
// Store query data with resolver reference for proper cleanup
|
|
this.activeQueries.set(queryId, { handler, errorHandler, timeout, resolver });
|
|
resolver.once('message', handler);
|
|
resolver.once('error', errorHandler);
|
|
|
|
const encodedQuery = dnsPacket.encode(query);
|
|
resolver.send(encodedQuery, 53, publicDNSServer, (err) => {
|
|
if (err) {
|
|
clearTimeout(timeout);
|
|
logWarn('DNSPool', `Error forwarding DNS query to ${publicDNSServer}: ${err.message}`);
|
|
resolver.removeListener('message', handler);
|
|
resolver.removeListener('error', errorHandler);
|
|
cleanup();
|
|
resolve(null);
|
|
}
|
|
});
|
|
});
|
|
|
|
// If we got a successful response, return it
|
|
if (result) {
|
|
return result;
|
|
}
|
|
|
|
// If this was the last server, return null
|
|
if (isLastServer) {
|
|
logWarn('DNSPool', `All DNS servers failed for ${query.questions[0].name}`);
|
|
return null;
|
|
}
|
|
|
|
// Otherwise, try next server
|
|
logDebug('DNSPool', `DNS server ${publicDNSServer} failed, trying next server...`);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Close all resolvers
|
|
*/
|
|
close() {
|
|
// Clean up all active queries and their listeners
|
|
for (const [queryId, query] of this.activeQueries.entries()) {
|
|
if (query.timeout) {
|
|
clearTimeout(query.timeout);
|
|
}
|
|
// Remove listeners from resolver
|
|
if (query.resolver && query.handler) {
|
|
try {
|
|
query.resolver.removeListener('message', query.handler);
|
|
} catch (err) {
|
|
logDebug('DNSPool', `Error removing message listener: ${err.message}`);
|
|
}
|
|
}
|
|
if (query.resolver && query.errorHandler) {
|
|
try {
|
|
query.resolver.removeListener('error', query.errorHandler);
|
|
} catch (err) {
|
|
logDebug('DNSPool', `Error removing error listener: ${err.message}`);
|
|
}
|
|
}
|
|
}
|
|
this.activeQueries.clear();
|
|
|
|
// Close all resolvers and remove all listeners
|
|
for (const resolver of this.resolvers) {
|
|
try {
|
|
// Remove all listeners to prevent leaks
|
|
resolver.removeAllListeners();
|
|
resolver.close();
|
|
} catch (err) {
|
|
logError('DNSPool', `Error closing resolver: ${err.message}`);
|
|
}
|
|
}
|
|
this.resolvers = [];
|
|
}
|
|
}
|
|
|
|
// Global DNS pool instance
|
|
const dnsPool = new DNSPool(parseInt(process.env.DNS_POOL_SIZE || '5', 10));
|
|
|
|
/**
|
|
* Update DNS pool configuration at runtime
|
|
* @param {number} maxConnections - New maximum number of connections
|
|
*/
|
|
function updateDnsPool(maxConnections) {
|
|
const oldMax = dnsPool.maxConnections;
|
|
dnsPool.maxConnections = maxConnections;
|
|
|
|
// If reducing pool size, close excess resolvers
|
|
if (maxConnections < oldMax && dnsPool.resolvers.length > maxConnections) {
|
|
const excessResolvers = dnsPool.resolvers.splice(maxConnections);
|
|
for (const resolver of excessResolvers) {
|
|
try {
|
|
resolver.removeAllListeners();
|
|
resolver.close();
|
|
} catch (err) {
|
|
logError('DNSPool', `Error closing excess resolver: ${err.message}`);
|
|
}
|
|
}
|
|
logInfo('DNSPool', `Reduced pool size from ${oldMax} to ${maxConnections}, closed ${excessResolvers.length} resolvers`);
|
|
} else if (maxConnections > oldMax) {
|
|
logInfo('DNSPool', `Increased pool size from ${oldMax} to ${maxConnections}, new resolvers will be created on demand`);
|
|
}
|
|
|
|
// PUBLIC_DNS_SERVER is already read from process.env at query time, so no action needed
|
|
}
|
|
|
|
module.exports = { dnsPool, DNSPool, updateDnsPool };
|
|
|