Files
p2ns/includes/networking/dns.js
T
2025-12-17 20:05:50 -05:00

353 lines
13 KiB
JavaScript

const dgram = require('dgram');
const dnsPacket = require('dns-packet');
const { getHashForDomain } = require('../core/core');
const state = require('../infrastructure/state');
const { logDebug, logError, logWarn, logInfo } = require('../infrastructure/logger');
const { createInterfaceForDomain } = require('./virtual_interfaces');
const { startHolesailClient } = require('./holesail');
const { dnsPool } = require('./dns_pool');
const { trackDNSQuery, trackDNSQueryWithTiming } = require('../maintenance/metrics');
const { getCircuitBreaker } = require('../infrastructure/circuit_breaker');
const { secondsToMs } = require('../infrastructure/utils');
const dnsServer = dgram.createSocket('udp4');
// Initialize state for domains with both
if (!state.domainsWithBoth) state.domainsWithBoth = new Set();
if (!state.publicIpForDomain) state.publicIpForDomain = {};
if (!state.versionPreferences) state.versionPreferences = new Map();
// DNS resolution cache with TTL
const dnsCache = new Map(); // domain -> { p2pHash: string|null, publicIP: string|null, timestamp: number, ttl: number }
const DEFAULT_CACHE_TTL = secondsToMs(30); // 30 seconds default TTL
const MAX_DNS_CACHE_SIZE = parseInt(process.env.MAX_DNS_CACHE_SIZE || '5000', 10); // Default 5000 entries
function getCachedDNSResolution(domain) {
const cached = dnsCache.get(domain);
if (!cached) return null;
const now = Date.now();
if (now - cached.timestamp > cached.ttl) {
dnsCache.delete(domain);
return null;
}
return cached;
}
function setCachedDNSResolution(domain, p2pHash, publicIP, ttl = DEFAULT_CACHE_TTL) {
// If cache is at limit, remove oldest entries (simple FIFO - remove first key)
if (dnsCache.size >= MAX_DNS_CACHE_SIZE) {
const firstKey = dnsCache.keys().next().value;
if (firstKey) {
dnsCache.delete(firstKey);
logDebug('DNS', `Removed oldest DNS entry from cache (${firstKey}) to maintain size limit`);
}
}
dnsCache.set(domain, {
p2pHash,
publicIP,
timestamp: Date.now(),
ttl
});
}
// Cleanup old cache entries periodically
let dnsCacheCleanupInterval = null;
function startDNSCacheCleanup() {
if (dnsCacheCleanupInterval) {
clearInterval(dnsCacheCleanupInterval);
}
dnsCacheCleanupInterval = setInterval(() => {
const now = Date.now();
let cleanedCount = 0;
for (const [domain, cached] of dnsCache.entries()) {
if (now - cached.timestamp > cached.ttl) {
dnsCache.delete(domain);
cleanedCount++;
}
}
// If cache is still too large after TTL cleanup, remove oldest entries
if (dnsCache.size > MAX_DNS_CACHE_SIZE) {
const entriesToRemove = dnsCache.size - MAX_DNS_CACHE_SIZE;
const keysToRemove = Array.from(dnsCache.keys()).slice(0, entriesToRemove);
for (const key of keysToRemove) {
dnsCache.delete(key);
cleanedCount++;
}
logDebug('DNS', `Removed ${entriesToRemove} oldest DNS entries to maintain size limit`);
}
if (cleanedCount > 0) {
logDebug('DNS', `DNS cache cleanup: removed ${cleanedCount} entries, ${dnsCache.size} remaining`);
}
}, secondsToMs(60)); // Cleanup every minute
}
function stopDNSCacheCleanup() {
if (dnsCacheCleanupInterval) {
clearInterval(dnsCacheCleanupInterval);
dnsCacheCleanupInterval = null;
}
}
// Start cleanup on module load
startDNSCacheCleanup();
// Circuit breaker for public DNS queries
const publicDNSCircuitBreaker = getCircuitBreaker('public-dns', {
failureThreshold: 5,
resetTimeout: secondsToMs(30)
});
// Check public DNS for fallback resolution using connection pool
async function checkPublicDNS(query) {
try {
const response = await publicDNSCircuitBreaker.execute(async () => {
return await dnsPool.query(query);
}, 'public-dns');
if (response && response.answers && response.answers.length > 0) {
trackDNSQuery('public');
} else {
trackDNSQuery('failure');
}
return response;
} catch (err) {
// Circuit breaker is open or query failed
logWarn('DNS', `Public DNS query failed (circuit breaker may be open): ${err.message}`);
trackDNSQuery('failure');
// Return null to indicate failure, allow graceful degradation
return null;
}
}
// Handle incoming DNS messages
async function handleDnsMessage(msg, rinfo) {
const query = dnsPacket.decode(msg);
const domain = query.questions[0].name.toLowerCase();
const type = query.questions[0].type;
logInfo('DNS', `DNS query received: Domain = ${domain}, Type = ${type}`);
// Check local DNS records first
const localAnswers = (state.localDnsRecords || []).filter(r => r.name.toLowerCase() === domain && r.type === type);
if (localAnswers.length > 0) {
const response = dnsPacket.encode({
type: 'response',
id: query.id,
questions: query.questions,
answers: localAnswers
});
dnsServer.send(response, rinfo.port, rinfo.address);
return;
}
// Handle internal domains
let isInternal = false;
try {
const { getInternalDomains } = require('../plugins/plugin-handler');
const internalDomains = await getInternalDomains();
isInternal = internalDomains.includes(domain);
} catch (err) {
// Fallback: only p2ns.admin is internal
isInternal = domain === 'p2ns.admin';
}
if (isInternal) {
const localIP = '127.0.0.1';
state.domainToIPMap[domain] = localIP;
const response = dnsPacket.encode({
type: 'response',
id: query.id,
questions: query.questions,
answers: [{
type: 'A',
name: domain,
ttl: 1,
data: localIP
}]
});
dnsServer.send(response, rinfo.port, rinfo.address);
return;
}
if (!state.dnsPass) {
logWarn('DNS', 'dnsPass not initialized, handling as public DNS');
const publicDNSResponse = await checkPublicDNS(query);
// Graceful degradation: if public DNS fails, return empty response instead of error
if (publicDNSResponse) {
const response = dnsPacket.encode({
type: 'response',
id: query.id,
flags: publicDNSResponse.flags,
questions: query.questions,
answers: publicDNSResponse.answers.map(answer => ({
...answer,
ttl: 1 // Force TTL to 1 second
})),
authorities: publicDNSResponse.authorities || [],
additionals: publicDNSResponse.additionals || []
});
dnsServer.send(response, rinfo.port, rinfo.address);
} else {
const response = dnsPacket.encode({
type: 'response',
id: query.id,
questions: query.questions,
answers: []
});
dnsServer.send(response, rinfo.port, rinfo.address);
}
return;
}
// Check cache first
const cached = getCachedDNSResolution(domain);
let hash = null;
let p2pRecord = null;
let publicDNSResponse = null;
if (cached) {
logDebug('DNS', `Using cached DNS resolution for ${domain}`);
hash = cached.p2pHash;
if (hash) {
p2pRecord = { hash };
}
// For public DNS, we still need to query if not cached or if we need full response
// But we can use cached publicIP if available
if (cached.publicIP) {
state.publicIpForDomain[domain] = cached.publicIP;
}
} else {
// Not cached, perform lookups
hash = await getHashForDomain(domain);
logDebug('DNS', `Resolved hash for ${domain}: ${hash || 'null'}`);
if (hash) {
p2pRecord = { hash };
}
publicDNSResponse = await checkPublicDNS(query);
// Cache the results
let publicIP = null;
if (publicDNSResponse && publicDNSResponse.answers.length > 0) {
publicIP = publicDNSResponse.answers.find(a => a.type === 'A')?.data;
}
setCachedDNSResolution(domain, hash, publicIP, DEFAULT_CACHE_TTL);
}
// If we used cache but need public DNS response, query it
if (cached && !publicDNSResponse) {
publicDNSResponse = await checkPublicDNS(query);
// Update cache with fresh public IP
if (publicDNSResponse && publicDNSResponse.answers.length > 0) {
const publicIP = publicDNSResponse.answers.find(a => a.type === 'A')?.data;
if (publicIP) {
setCachedDNSResolution(domain, hash, publicIP, DEFAULT_CACHE_TTL);
}
}
}
// Check if domain has both P2P and public records
let publicIP = null;
if (publicDNSResponse && publicDNSResponse.answers.length > 0) {
publicIP = publicDNSResponse.answers.find(a => a.type === 'A')?.data;
if (publicIP) {
state.publicIpForDomain[domain] = publicIP;
if (p2pRecord) {
state.domainsWithBoth.add(domain);
logInfo('DNS', `Domain ${domain} has both P2P and public records.`);
}
}
}
// Check user preference for version
const version = state.versionPreferences.get(domain);
if (p2pRecord && (!state.domainsWithBoth.has(domain) || version !== 'public')) {
const startTime = Date.now();
const localIP = state.domainToIPMap[domain] || await createInterfaceForDomain(domain);
if (!localIP) {
logError('DNS', `Failed to assign IP for ${domain}`);
trackDNSQuery('failure');
const response = dnsPacket.encode({
type: 'response',
id: query.id,
questions: query.questions,
answers: []
});
dnsServer.send(response, rinfo.port, rinfo.address);
return;
}
await startHolesailClient(domain, p2pRecord.hash, localIP, state.internalPort);
const responseTime = Date.now() - startTime;
trackDNSQueryWithTiming('p2p', domain, responseTime);
const response = dnsPacket.encode({
type: 'response',
id: query.id,
questions: query.questions,
answers: [{
type: 'A',
name: domain,
ttl: 1,
data: localIP
}]
});
dnsServer.send(response, rinfo.port, rinfo.address);
} else if (publicDNSResponse && publicDNSResponse.answers.length > 0 && (!p2pRecord || version === 'public')) {
trackDNSQueryWithTiming('public', domain, null); // Response time tracked in checkPublicDNS
const response = dnsPacket.encode({
type: 'response',
id: query.id,
flags: publicDNSResponse.flags,
questions: query.questions,
answers: publicDNSResponse.answers.map(answer => ({
...answer,
ttl: 1 // Force TTL to 1 second
})),
authorities: publicDNSResponse.authorities || [],
additionals: publicDNSResponse.additionals || []
});
dnsServer.send(response, rinfo.port, rinfo.address);
} else {
trackDNSQueryWithTiming('failure', domain, null);
logWarn('DNS', `No P2P or public DNS records found for ${domain}`);
const response = dnsPacket.encode({
type: 'response',
id: query.id,
questions: query.questions,
answers: []
});
dnsServer.send(response, rinfo.port, rinfo.address);
}
}
// Bind DNS server to port
function bindDnsServer() {
if (process.env.DISABLE_DNS_SERVER === 'true') {
logInfo('DNS', 'DNS server disabled via DISABLE_DNS_SERVER=true');
return;
}
dnsServer.on('listening', () => {
logInfo('DNS', `DNS Server running on port ${process.env.DNS_PORT || 53}, bound to 0.0.0.0`);
});
dnsServer.on('error', (err) => {
logError('DNS', `Failed to bind DNS server to port ${process.env.DNS_PORT || 53}: ${err.message}`);
process.exit(1);
});
dnsServer.on('message', handleDnsMessage);
dnsServer.bind(process.env.DNS_PORT || 53, '0.0.0.0');
}
// Close DNS server and remove listeners
function closeDnsServer() {
if (process.env.DISABLE_DNS_SERVER === 'true') {
return;
}
try {
dnsServer.removeAllListeners();
dnsServer.close();
logInfo('DNS', 'DNS server closed and listeners removed');
} catch (err) {
logError('DNS', `Error closing DNS server: ${err.message}`);
}
}
module.exports = { dnsServer, handleDnsMessage, bindDnsServer, closeDnsServer, stopDNSCacheCleanup };