feat(smartdns): Add DNS Server and DNSSEC tools with comprehensive unit tests
This commit is contained in:
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
83
ts_server/classes.dnstools.ts
Normal file
83
ts_server/classes.dnstools.ts
Normal 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];
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './classes.dnsserver.js';
|
@ -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,
|
||||
}
|
||||
|
Reference in New Issue
Block a user