feat(smartdns): Add DNS Server and DNSSEC tools with comprehensive unit tests

This commit is contained in:
2024-09-18 19:28:28 +02:00
parent 2cfecab96f
commit 5c06ae1edb
10 changed files with 1038 additions and 389 deletions

View File

@ -7,59 +7,61 @@ interface IDnsServerOptions {
udpPort: number;
}
class DnsServer {
interface IDnsHandler {
domainPattern: string;
recordTypes: string[];
handler: (question: plugins.dnsPacket.Question) => plugins.dnsPacket.Answer | null;
}
export class DnsServer {
private udpServer: plugins.dgram.Socket;
private httpsServer: plugins.https.Server;
private handlers: IDnsHandler[] = [];
constructor(private options: IDnsServerOptions) {
this.udpServer = plugins.dgram.createSocket('udp4');
this.setupUdpServer();
constructor(private options: IDnsServerOptions) {}
this.httpsServer = plugins.https.createServer(
{
key: plugins.fs.readFileSync(options.httpsKey),
cert: plugins.fs.readFileSync(options.httpsCert)
},
this.handleHttpsRequest.bind(this)
);
public registerHandler(
domainPattern: string,
recordTypes: string[],
handler: (question: plugins.dnsPacket.Question) => plugins.dnsPacket.Answer | null
): void {
this.handlers.push({ domainPattern, recordTypes, handler });
}
private setupUdpServer(): void {
this.udpServer.on('message', (msg, rinfo) => {
const request = plugins.dnsPacket.decode(msg);
const response = {
type: 'response' as const,
id: request.id,
flags: plugins.dnsPacket.RECURSION_DESIRED | plugins.dnsPacket.RECURSION_AVAILABLE,
questions: request.questions,
answers: [] as plugins.dnsPacket.Answer[]
};
private processDnsRequest(request: plugins.dnsPacket.Packet): plugins.dnsPacket.Packet {
const response: plugins.dnsPacket.Packet = {
type: 'response',
id: request.id,
flags: plugins.dnsPacket.RECURSION_DESIRED | plugins.dnsPacket.RECURSION_AVAILABLE,
questions: request.questions,
answers: [],
};
const question = request.questions[0];
console.log(`UDP query for ${question.name} of type ${question.type}`);
for (const question of request.questions) {
console.log(`Query for ${question.name} of type ${question.type}`);
if (question.type === 'A') {
response.answers.push({
name: question.name,
type: 'A',
class: 'IN',
ttl: 300,
data: '127.0.0.1'
});
let answered = false;
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) {
response.answers.push(answer);
answered = true;
break;
}
}
}
const responseData = plugins.dnsPacket.encode(response);
this.udpServer.send(responseData, rinfo.port, rinfo.address);
});
if (!answered) {
console.log(`No handler found for ${question.name} of type ${question.type}`);
}
}
this.udpServer.on('error', (err) => {
console.error(`UDP Server error:\n${err.stack}`);
this.udpServer.close();
});
this.udpServer.bind(this.options.udpPort, '0.0.0.0', () => {
console.log(`UDP DNS server running on port ${this.options.udpPort}`);
});
return response;
}
private handleHttpsRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
@ -71,27 +73,7 @@ class DnsServer {
}).on('end', () => {
const msg = Buffer.concat(body);
const request = plugins.dnsPacket.decode(msg);
const response = {
type: 'response' as const,
id: request.id,
flags: plugins.dnsPacket.RECURSION_DESIRED | plugins.dnsPacket.RECURSION_AVAILABLE,
questions: request.questions,
answers: [] as plugins.dnsPacket.Answer[]
};
const question = request.questions[0];
console.log(`DoH query for ${question.name} of type ${question.type}`);
if (question.type === 'A') {
response.answers.push({
name: question.name,
type: 'A',
class: 'IN',
ttl: 300,
data: '127.0.0.1'
});
}
const response = this.processDnsRequest(request);
const responseData = plugins.dnsPacket.encode(response);
res.writeHead(200, { 'Content-Type': 'application/dns-message' });
res.end(responseData);
@ -102,19 +84,64 @@ class DnsServer {
}
}
public start(): void {
this.httpsServer.listen(this.options.httpsPort, () => {
console.log(`DoH server running on port ${this.options.httpsPort}`);
public async start(): Promise<void> {
this.httpsServer = plugins.https.createServer(
{
key: this.options.httpsKey,
cert: this.options.httpsCert,
},
this.handleHttpsRequest.bind(this)
);
this.udpServer = plugins.dgram.createSocket('udp4');
this.udpServer.on('message', (msg, rinfo) => {
const request = plugins.dnsPacket.decode(msg);
const response = this.processDnsRequest(request);
const responseData = plugins.dnsPacket.encode(response);
this.udpServer.send(responseData, rinfo.port, rinfo.address);
});
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, '0.0.0.0', () => {
console.log(`UDP DNS server running on port ${this.options.udpPort}`);
udpListeningDeferred.resolve();
});
this.httpsServer.listen(this.options.httpsPort, () => {
console.log(`HTTPS DNS server running on port ${this.options.httpsPort}`);
httpsListeningDeferred.resolve();
});
} catch (err) {
console.error('Error starting DNS server:', err);
process.exit(1);
}
await Promise.all([udpListeningDeferred.promise, httpsListeningDeferred.promise]);
}
public stop(): void {
public async stop(): Promise<void> {
const doneUdp = plugins.smartpromise.defer<void>();
const doneHttps = plugins.smartpromise.defer<void>();
this.udpServer.close(() => {
console.log('UDP DNS server stopped');
this.udpServer.unref();
this.udpServer = null;
doneUdp.resolve();
});
this.httpsServer.close(() => {
console.log('HTTPS DNS server stopped');
this.httpsServer.unref();
this.httpsServer = null;
doneHttps.resolve();
});
await Promise.all([doneUdp.promise, doneHttps.promise]);
}
}
}

View File

@ -0,0 +1,83 @@
import * as plugins from './plugins.js';
interface DnssecZone {
zone: string;
algorithm: string;
keySize: number;
days: number;
}
interface DnssecKeyPair {
private: string;
public: string;
}
class DnsSec {
private zone: DnssecZone;
private keyPair: DnssecKeyPair;
private ec: any; // declare the ec instance
constructor(zone: DnssecZone) {
this.zone = zone;
this.ec = new plugins.elliptic.ec('secp256k1'); // Create an instance of the secp256k1 curve
this.keyPair = this.generateKeyPair();
}
private generateKeyPair(): DnssecKeyPair {
const key = this.ec.genKeyPair();
const privatePem = key.getPrivate().toString('hex'); // get private key in hex format
// @ts-ignore
const publicPem = key.getPublic().toString('hex'); // get public key in hex format
return {
private: privatePem,
public: publicPem
};
}
private formatPEM(pem: string, type: string): string {
const start = `-----BEGIN ${type}-----`;
const end = `-----END ${type}-----`;
const formatted = [start];
for (let i = 0; i < pem.length; i += 64) {
formatted.push(pem.slice(i, i + 64));
}
formatted.push(end);
return formatted.join('\n');
}
public getDSRecord(): string {
const publicPem = this.keyPair.public;
const publicKey = this.ec.keyFromPublic(publicPem); // Create a public key from the publicPEM
const digest = publicKey.getPublic(); // get public point
return `DS {id} 8 {algorithm} {digest} {hash-algorithm}\n`
.replace('{id}', '256') // zone hash
.replace('{algorithm}', this.getAlgorithm())
.replace('{digest}', `0x${digest.getX()}${digest.getY()}`)
.replace('{hash-algorithm}', '2');
}
private getAlgorithm(): string {
switch (this.zone.algorithm) {
case 'ECDSA':
return '8';
case 'ED25519':
return '15';
case 'RSA':
return '1';
default:
throw new Error(`Unsupported algorithm: ${this.zone.algorithm}`);
}
}
public getKeyPair(): DnssecKeyPair {
return this.keyPair;
}
public getDsAndKeyPair(): [DnssecKeyPair, string] {
const dsRecord = this.getDSRecord();
return [this.keyPair, dsRecord];
}
}

View File

@ -0,0 +1 @@
export * from './classes.dnsserver.js';

View File

@ -1,3 +1,4 @@
// node native
import fs from 'fs';
import http from 'http';
import https from 'https';
@ -10,8 +11,20 @@ export {
dgram,
}
import * as dnsPacket from 'dns-packet';
// @push.rocks scope
import * as smartpromise from '@push.rocks/smartpromise';
export {
dnsPacket
smartpromise,
}
// third party
import * as elliptic from 'elliptic';
import * as dnsPacket from 'dns-packet';
import * as minimatch from 'minimatch';
export {
dnsPacket,
elliptic,
minimatch,
}