feat(dnssec): Introduced DNSSEC support with ECDSA algorithm
This commit is contained in:
parent
5278c2ce78
commit
74ac0c1287
@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2024-09-19 - 6.2.0 - feat(dnssec)
|
||||
Introduced DNSSEC support with ECDSA algorithm
|
||||
|
||||
- Added `DnsSec` class for handling DNSSEC operations.
|
||||
- Updated `DnsServer` to support DNSSEC with ECDSA.
|
||||
- Shifted DNS-related helper functions to `DnsServer` class.
|
||||
- Integrated parsing and handling of DNSKEY and RRSIG records in `DnsServer`.
|
||||
|
||||
## 2024-09-19 - 6.1.1 - fix(ts_server)
|
||||
Update DnsSec class to fully implement key generation and DNSKEY record creation.
|
||||
|
||||
|
@ -69,7 +69,7 @@ export class DnsSec {
|
||||
return { privateKey, publicKey };
|
||||
}
|
||||
|
||||
private getAlgorithmNumber(): number {
|
||||
public getAlgorithmNumber(): number {
|
||||
switch (this.zone.algorithm) {
|
||||
case 'ECDSA':
|
||||
return 13; // ECDSAP256SHA256
|
@ -1,16 +1,46 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { DnsSec } from './classes.dnssec.js';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
|
||||
interface IDnsServerOptions {
|
||||
httpsKey: string;
|
||||
httpsCert: string;
|
||||
httpsPort: number;
|
||||
udpPort: number;
|
||||
dnssecZone: string;
|
||||
}
|
||||
|
||||
interface DnsAnswer {
|
||||
name: string;
|
||||
type: string;
|
||||
class: string | number;
|
||||
ttl: number;
|
||||
data: any;
|
||||
}
|
||||
|
||||
interface IDnsHandler {
|
||||
domainPattern: string;
|
||||
recordTypes: string[];
|
||||
handler: (question: plugins.dnsPacket.Question) => plugins.dnsPacket.Answer | null;
|
||||
handler: (question: dnsPacket.Question) => DnsAnswer | null;
|
||||
}
|
||||
|
||||
// Define types for DNSSEC records if not provided
|
||||
interface DNSKEYData {
|
||||
flags: number;
|
||||
algorithm: number;
|
||||
key: Buffer;
|
||||
}
|
||||
|
||||
interface RRSIGData {
|
||||
typeCovered: string; // Changed to string to match dns-packet expectations
|
||||
algorithm: number;
|
||||
labels: number;
|
||||
originalTTL: number;
|
||||
expiration: number;
|
||||
inception: number;
|
||||
keyTag: number;
|
||||
signerName: string;
|
||||
signature: Buffer;
|
||||
}
|
||||
|
||||
export class DnsServer {
|
||||
@ -18,30 +48,75 @@ export class DnsServer {
|
||||
private httpsServer: plugins.https.Server;
|
||||
private handlers: IDnsHandler[] = [];
|
||||
|
||||
constructor(private options: IDnsServerOptions) {}
|
||||
// DNSSEC related properties
|
||||
private dnsSec: DnsSec;
|
||||
private dnskeyRecord: DNSKEYData;
|
||||
private keyTag: number;
|
||||
|
||||
constructor(private options: IDnsServerOptions) {
|
||||
// Initialize DNSSEC
|
||||
this.dnsSec = new DnsSec({
|
||||
zone: options.dnssecZone,
|
||||
algorithm: 'ECDSA', // You can change this based on your needs
|
||||
keySize: 256,
|
||||
days: 365,
|
||||
});
|
||||
|
||||
// Generate DNSKEY and DS records
|
||||
const { dsRecord, dnskeyRecord } = this.dnsSec.getDsAndKeyPair();
|
||||
|
||||
// Parse DNSKEY record into dns-packet format
|
||||
this.dnskeyRecord = this.parseDNSKEYRecord(dnskeyRecord);
|
||||
this.keyTag = this.computeKeyTag(this.dnskeyRecord);
|
||||
}
|
||||
|
||||
public registerHandler(
|
||||
domainPattern: string,
|
||||
recordTypes: string[],
|
||||
handler: (question: plugins.dnsPacket.Question) => plugins.dnsPacket.Answer | null
|
||||
handler: (question: dnsPacket.Question) => DnsAnswer | null
|
||||
): void {
|
||||
this.handlers.push({ domainPattern, recordTypes, handler });
|
||||
}
|
||||
|
||||
private processDnsRequest(request: plugins.dnsPacket.Packet): plugins.dnsPacket.Packet {
|
||||
const response: plugins.dnsPacket.Packet = {
|
||||
private processDnsRequest(request: dnsPacket.Packet): dnsPacket.Packet {
|
||||
const response: dnsPacket.Packet = {
|
||||
type: 'response',
|
||||
id: request.id,
|
||||
flags: plugins.dnsPacket.RECURSION_DESIRED | plugins.dnsPacket.RECURSION_AVAILABLE,
|
||||
flags:
|
||||
dnsPacket.AUTHORITATIVE_ANSWER |
|
||||
dnsPacket.RECURSION_AVAILABLE |
|
||||
(request.flags & dnsPacket.RECURSION_DESIRED ? dnsPacket.RECURSION_DESIRED : 0),
|
||||
questions: request.questions,
|
||||
answers: [],
|
||||
additionals: [],
|
||||
};
|
||||
|
||||
const dnssecRequested = this.isDnssecRequested(request);
|
||||
|
||||
for (const question of request.questions) {
|
||||
console.log(`Query for ${question.name} of type ${question.type}`);
|
||||
|
||||
let answered = false;
|
||||
|
||||
// Handle DNSKEY queries if DNSSEC is requested
|
||||
if (dnssecRequested && question.type === 'DNSKEY' && question.name === this.options.dnssecZone) {
|
||||
const dnskeyAnswer: DnsAnswer = {
|
||||
name: question.name,
|
||||
type: 'DNSKEY',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: this.dnskeyRecord,
|
||||
};
|
||||
response.answers.push(dnskeyAnswer as plugins.dnsPacket.Answer);
|
||||
|
||||
// Sign the DNSKEY RRset
|
||||
const rrsig = this.generateRRSIG('DNSKEY', [dnskeyAnswer], question.name);
|
||||
response.answers.push(rrsig as plugins.dnsPacket.Answer);
|
||||
|
||||
answered = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const handlerEntry of this.handlers) {
|
||||
if (
|
||||
plugins.minimatch.minimatch(question.name, handlerEntry.domainPattern) &&
|
||||
@ -49,7 +124,20 @@ export class DnsServer {
|
||||
) {
|
||||
const answer = handlerEntry.handler(question);
|
||||
if (answer) {
|
||||
response.answers.push(answer);
|
||||
// Ensure the answer has ttl and class
|
||||
const dnsAnswer: DnsAnswer = {
|
||||
...answer,
|
||||
ttl: answer.ttl || 300,
|
||||
class: answer.class || 'IN',
|
||||
};
|
||||
response.answers.push(dnsAnswer as plugins.dnsPacket.Answer);
|
||||
|
||||
if (dnssecRequested) {
|
||||
// Sign the answer RRset
|
||||
const rrsig = this.generateRRSIG(question.type, [dnsAnswer], question.name);
|
||||
response.answers.push(rrsig as plugins.dnsPacket.Answer);
|
||||
}
|
||||
|
||||
answered = true;
|
||||
break;
|
||||
}
|
||||
@ -58,23 +146,193 @@ export class DnsServer {
|
||||
|
||||
if (!answered) {
|
||||
console.log(`No handler found for ${question.name} of type ${question.type}`);
|
||||
response.flags |= dnsPacket.AUTHORITATIVE_ANSWER;
|
||||
const soaAnswer: DnsAnswer = {
|
||||
name: question.name,
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: {
|
||||
mname: `ns1.${this.options.dnssecZone}`,
|
||||
rname: `hostmaster.${this.options.dnssecZone}`,
|
||||
serial: Math.floor(Date.now() / 1000),
|
||||
refresh: 3600,
|
||||
retry: 600,
|
||||
expire: 604800,
|
||||
minimum: 86400,
|
||||
},
|
||||
};
|
||||
response.answers.push(soaAnswer as plugins.dnsPacket.Answer);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private isDnssecRequested(request: dnsPacket.Packet): boolean {
|
||||
if (!request.additionals) return false;
|
||||
for (const additional of request.additionals) {
|
||||
if (additional.type === 'OPT' && typeof additional.flags === 'number') {
|
||||
// The DO bit is the 15th bit (0x8000)
|
||||
if (additional.flags & 0x8000) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private generateRRSIG(
|
||||
type: string,
|
||||
rrset: DnsAnswer[],
|
||||
name: string
|
||||
): DnsAnswer {
|
||||
// Prepare RRSIG data
|
||||
const algorithm = this.dnsSec.getAlgorithmNumber();
|
||||
const keyTag = this.keyTag;
|
||||
const signerName = this.options.dnssecZone.endsWith('.') ? this.options.dnssecZone : `${this.options.dnssecZone}.`;
|
||||
const inception = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago
|
||||
const expiration = inception + 86400; // Valid for 1 day
|
||||
const ttl = rrset[0].ttl || 300;
|
||||
|
||||
// Serialize the RRset in canonical form
|
||||
const rrsetBuffer = this.serializeRRset(rrset);
|
||||
|
||||
// Sign the RRset
|
||||
const signature = this.dnsSec.signData(rrsetBuffer);
|
||||
|
||||
// Construct the RRSIG record
|
||||
const rrsig: DnsAnswer = {
|
||||
name,
|
||||
type: 'RRSIG',
|
||||
class: 'IN',
|
||||
ttl,
|
||||
data: {
|
||||
typeCovered: type, // Changed to type string
|
||||
algorithm,
|
||||
labels: name.split('.').length - 1,
|
||||
originalTTL: ttl,
|
||||
expiration,
|
||||
inception,
|
||||
keyTag,
|
||||
signerName,
|
||||
signature: signature,
|
||||
},
|
||||
};
|
||||
|
||||
return rrsig;
|
||||
}
|
||||
|
||||
private serializeRRset(rrset: DnsAnswer[]): Buffer {
|
||||
// Implement canonical DNS RRset serialization as per RFC 4034 Section 6
|
||||
const buffers: Buffer[] = [];
|
||||
for (const rr of rrset) {
|
||||
if (rr.type === 'OPT') {
|
||||
continue; // Skip OPT records
|
||||
}
|
||||
|
||||
const name = rr.name.endsWith('.') ? rr.name : rr.name + '.';
|
||||
const nameBuffer = this.nameToBuffer(name.toLowerCase());
|
||||
|
||||
const typeValue = this.qtypeToNumber(rr.type);
|
||||
const typeBuffer = Buffer.alloc(2);
|
||||
typeBuffer.writeUInt16BE(typeValue, 0);
|
||||
|
||||
const classValue = this.classToNumber(rr.class);
|
||||
const classBuffer = Buffer.alloc(2);
|
||||
classBuffer.writeUInt16BE(classValue, 0);
|
||||
|
||||
const ttlValue = rr.ttl || 300;
|
||||
const ttlBuffer = Buffer.alloc(4);
|
||||
ttlBuffer.writeUInt32BE(ttlValue, 0);
|
||||
|
||||
// Serialize the data based on the record type
|
||||
const dataBuffer = this.serializeRData(rr.type, rr.data);
|
||||
|
||||
const rdLengthBuffer = Buffer.alloc(2);
|
||||
rdLengthBuffer.writeUInt16BE(dataBuffer.length, 0);
|
||||
|
||||
buffers.push(Buffer.concat([nameBuffer, typeBuffer, classBuffer, ttlBuffer, rdLengthBuffer, dataBuffer]));
|
||||
}
|
||||
return Buffer.concat(buffers);
|
||||
}
|
||||
|
||||
private serializeRData(type: string, data: any): Buffer {
|
||||
// Implement serialization for each record type you support
|
||||
switch (type) {
|
||||
case 'A':
|
||||
return Buffer.from(data.split('.').map((octet: string) => parseInt(octet, 10)));
|
||||
case 'AAAA':
|
||||
// Handle IPv6 addresses
|
||||
return Buffer.from(data.split(':').flatMap((segment: string) => {
|
||||
const num = parseInt(segment, 16);
|
||||
return [num >> 8, num & 0xff];
|
||||
}));
|
||||
case 'DNSKEY':
|
||||
const dnskeyData: DNSKEYData = data;
|
||||
return Buffer.concat([
|
||||
Buffer.from([dnskeyData.flags >> 8, dnskeyData.flags & 0xff]),
|
||||
Buffer.from([3]), // Protocol field, always 3
|
||||
Buffer.from([dnskeyData.algorithm]),
|
||||
dnskeyData.key,
|
||||
]);
|
||||
case 'SOA':
|
||||
// Implement SOA record serialization if needed
|
||||
// For now, return an empty buffer or handle as needed
|
||||
return Buffer.alloc(0);
|
||||
// Add cases for other record types as needed
|
||||
default:
|
||||
throw new Error(`Serialization for record type ${type} is not implemented.`);
|
||||
}
|
||||
}
|
||||
|
||||
private parseDNSKEYRecord(dnskeyRecord: string): DNSKEYData {
|
||||
// Parse the DNSKEY record string into dns-packet format
|
||||
const parts = dnskeyRecord.trim().split(/\s+/);
|
||||
const flags = parseInt(parts[3], 10);
|
||||
const algorithm = parseInt(parts[5], 10);
|
||||
const publicKeyBase64 = parts.slice(6).join('');
|
||||
const key = Buffer.from(publicKeyBase64, 'base64');
|
||||
|
||||
return {
|
||||
flags,
|
||||
algorithm,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
private computeKeyTag(dnskeyRecord: DNSKEYData): number {
|
||||
// Compute key tag as per RFC 4034 Appendix B
|
||||
const flags = dnskeyRecord.flags;
|
||||
const algorithm = dnskeyRecord.algorithm;
|
||||
const key = dnskeyRecord.key;
|
||||
|
||||
const dnskeyRdata = Buffer.concat([
|
||||
Buffer.from([flags >> 8, flags & 0xff]),
|
||||
Buffer.from([3]), // Protocol field, always 3
|
||||
Buffer.from([algorithm]),
|
||||
key,
|
||||
]);
|
||||
|
||||
let acc = 0;
|
||||
for (let i = 0; i < dnskeyRdata.length; i++) {
|
||||
acc += (i & 1) ? dnskeyRdata[i] : dnskeyRdata[i] << 8;
|
||||
}
|
||||
acc += (acc >> 16) & 0xffff;
|
||||
return acc & 0xffff;
|
||||
}
|
||||
|
||||
private handleHttpsRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
||||
if (req.method === 'POST' && req.url === '/dns-query') {
|
||||
let body: Buffer[] = [];
|
||||
|
||||
req.on('data', chunk => {
|
||||
req.on('data', (chunk) => {
|
||||
body.push(chunk);
|
||||
}).on('end', () => {
|
||||
const msg = Buffer.concat(body);
|
||||
const request = plugins.dnsPacket.decode(msg);
|
||||
const request = dnsPacket.decode(msg);
|
||||
const response = this.processDnsRequest(request);
|
||||
const responseData = plugins.dnsPacket.encode(response);
|
||||
const responseData = dnsPacket.encode(response);
|
||||
res.writeHead(200, { 'Content-Type': 'application/dns-message' });
|
||||
res.end(responseData);
|
||||
});
|
||||
@ -95,9 +353,9 @@ export class DnsServer {
|
||||
|
||||
this.udpServer = plugins.dgram.createSocket('udp4');
|
||||
this.udpServer.on('message', (msg, rinfo) => {
|
||||
const request = plugins.dnsPacket.decode(msg);
|
||||
const request = dnsPacket.decode(msg);
|
||||
const response = this.processDnsRequest(request);
|
||||
const responseData = plugins.dnsPacket.encode(response);
|
||||
const responseData = dnsPacket.encode(response);
|
||||
this.udpServer.send(responseData, rinfo.port, rinfo.address);
|
||||
});
|
||||
|
||||
@ -108,6 +366,7 @@ export class DnsServer {
|
||||
|
||||
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}`);
|
||||
@ -144,4 +403,49 @@ export class DnsServer {
|
||||
|
||||
await Promise.all([doneUdp.promise, doneHttps.promise]);
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private qtypeToNumber(type: string): number {
|
||||
const QTYPE_NUMBERS: { [key: string]: number } = {
|
||||
'A': 1,
|
||||
'NS': 2,
|
||||
'CNAME': 5,
|
||||
'SOA': 6,
|
||||
'PTR': 12,
|
||||
'MX': 15,
|
||||
'TXT': 16,
|
||||
'AAAA': 28,
|
||||
'SRV': 33,
|
||||
'DNSKEY': 48,
|
||||
'RRSIG': 46,
|
||||
// Add more as needed
|
||||
};
|
||||
return QTYPE_NUMBERS[type.toUpperCase()] || 0;
|
||||
}
|
||||
|
||||
private classToNumber(cls: string | number): number {
|
||||
const CLASS_NUMBERS: { [key: string]: number } = {
|
||||
'IN': 1,
|
||||
'CH': 3,
|
||||
'HS': 4,
|
||||
// Add more as needed
|
||||
};
|
||||
if (typeof cls === 'number') {
|
||||
return cls;
|
||||
}
|
||||
return CLASS_NUMBERS[cls.toUpperCase()] || 1;
|
||||
}
|
||||
|
||||
private nameToBuffer(name: string): Buffer {
|
||||
const labels = name.split('.');
|
||||
const buffers = labels.map(label => {
|
||||
const len = Buffer.byteLength(label, 'utf8');
|
||||
const buf = Buffer.alloc(1 + len);
|
||||
buf.writeUInt8(len, 0);
|
||||
buf.write(label, 1);
|
||||
return buf;
|
||||
});
|
||||
return Buffer.concat([...buffers, Buffer.from([0])]); // Add root label
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ export {
|
||||
}
|
||||
|
||||
// third party
|
||||
import * as elliptic from 'elliptic';
|
||||
import elliptic from 'elliptic';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
import * as minimatch from 'minimatch';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user