feat(dnsserver): Add manual socket mode support to enable external socket control for the DNS server.

This commit is contained in:
2025-05-28 19:16:54 +00:00
parent 46e51cd846
commit 62b6fa26fa
5 changed files with 209 additions and 262 deletions

View File

@ -10,6 +10,9 @@ export interface IDnsServerOptions {
dnssecZone: string;
udpBindInterface?: string;
httpsBindInterface?: string;
// New options for independent manual socket control
manualUdpMode?: boolean;
manualHttpsMode?: boolean;
}
export interface DnsAnswer {
@ -33,7 +36,6 @@ interface DNSKEYData {
key: Buffer;
}
// Let's Encrypt related interfaces
interface LetsEncryptOptions {
email?: string;
@ -51,6 +53,10 @@ export class DnsServer {
private dnskeyRecord: DNSKEYData;
private keyTag: number;
// Track if servers are initialized
private udpServerInitialized: boolean = false;
private httpsServerInitialized: boolean = false;
constructor(private options: IDnsServerOptions) {
// Initialize DNSSEC
this.dnsSec = new DnsSec({
@ -68,6 +74,118 @@ export class DnsServer {
this.keyTag = this.computeKeyTag(this.dnskeyRecord);
}
/**
* Initialize servers without binding to ports
* This is called automatically by start() or can be called manually
*/
public initializeServers(): void {
this.initializeUdpServer();
this.initializeHttpsServer();
}
/**
* Initialize UDP server without binding
*/
public initializeUdpServer(): void {
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
*/
public initializeHttpsServer(): void {
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
*/
public handleHttpsSocket(socket: plugins.net.Socket): void {
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
*/
public handleUdpMessage(
msg: Buffer,
rinfo: plugins.dgram.RemoteInfo,
responseCallback?: (response: Buffer, rinfo: plugins.dgram.RemoteInfo) => void
): void {
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
*/
public processRawDnsPacket(packet: Buffer): Buffer {
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;
}
}
public registerHandler(
domainPattern: string,
recordTypes: string[],
@ -256,8 +374,10 @@ export class DnsServer {
this.options.httpsCert = certificate;
this.options.httpsKey = privateKey;
// Restart HTTPS server with new certificate
await this.restartHttpsServer();
// 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) {
@ -325,11 +445,15 @@ export class DnsServer {
this.handleHttpsRequest.bind(this)
);
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`);
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;
}
}
@ -343,11 +467,15 @@ export class DnsServer {
this.handleHttpsRequest.bind(this)
);
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`);
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);
@ -695,6 +823,27 @@ export class DnsServer {
}
public async start(): Promise<void> {
// 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';
@ -707,47 +856,43 @@ export class DnsServer {
throw new Error(`Invalid HTTPS bind interface: ${this.options.httpsBindInterface}`);
}
this.httpsServer = plugins.https.createServer(
{
key: this.options.httpsKey,
cert: this.options.httpsCert,
},
this.handleHttpsRequest.bind(this)
);
const promises: Promise<void>[] = [];
// Create appropriate socket type based on interface
const socketType = this.isIPv6(udpInterface) ? 'udp6' : 'udp4';
this.udpServer = plugins.dgram.createSocket(socketType);
this.udpServer.on('message', (msg, rinfo) => {
const request = dnsPacket.decode(msg);
const response = this.processDnsRequest(request);
const responseData = dnsPacket.encode(response);
this.udpServer.send(responseData, rinfo.port, rinfo.address);
});
// Bind UDP if not in manual UDP mode
if (!udpManual) {
const udpListeningDeferred = plugins.smartpromise.defer<void>();
promises.push(udpListeningDeferred.promise);
this.udpServer.on('error', (err) => {
console.error(`UDP Server error:\n${err.stack}`);
this.udpServer.close();
});
const udpListeningDeferred = plugins.smartpromise.defer<void>();
const httpsListeningDeferred = plugins.smartpromise.defer<void>();
try {
this.udpServer.bind(this.options.udpPort, udpInterface, () => {
console.log(`UDP DNS server running on ${udpInterface}:${this.options.udpPort}`);
udpListeningDeferred.resolve();
});
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 DNS server:', err);
process.exit(1);
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<void>();
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);
}
await Promise.all([udpListeningDeferred.promise, httpsListeningDeferred.promise]);
}
public async stop(): Promise<void> {
@ -781,6 +926,8 @@ export class DnsServer {
}
await Promise.all([doneUdp.promise, doneHttps.promise]);
this.udpServerInitialized = false;
this.httpsServerInitialized = false;
}
// Helper methods

View File

@ -4,14 +4,16 @@ import dgram from 'dgram';
import fs from 'fs';
import http from 'http';
import https from 'https';
import * as net from 'net';
import * as path from 'path';
export {
crypto,
dgram,
fs,
http,
https,
dgram,
net,
path,
}