feat(dnsserver): Enhance DNSSEC RRset signing and add configurable primary nameserver
- Fix DNSSEC to properly sign entire RRsets together instead of individual records - Implement proper SOA record serialization according to RFC 1035 - Add primaryNameserver option to IDnsServerOptions for customizable SOA mname field - Add comprehensive tests for DNSSEC RRset signing and SOA record handling - Update documentation with v7.4.3 improvements Co-Authored-By: User <user@example.com>
This commit is contained in:
@ -13,6 +13,8 @@ export interface IDnsServerOptions {
|
||||
// New options for independent manual socket control
|
||||
manualUdpMode?: boolean;
|
||||
manualHttpsMode?: boolean;
|
||||
// Primary nameserver for SOA records (defaults to ns1.{dnssecZone})
|
||||
primaryNameserver?: string;
|
||||
}
|
||||
|
||||
export interface DnsAnswer {
|
||||
@ -559,11 +561,15 @@ export class DnsServer {
|
||||
};
|
||||
|
||||
const dnssecRequested = this.isDnssecRequested(request);
|
||||
|
||||
// Map to group records by type for proper DNSSEC RRset signing
|
||||
const rrsetMap = new Map<string, DnsAnswer[]>();
|
||||
|
||||
for (const question of request.questions) {
|
||||
console.log(`Query for ${question.name} of type ${question.type}`);
|
||||
|
||||
let answered = false;
|
||||
const recordsForQuestion: DnsAnswer[] = [];
|
||||
|
||||
// Handle DNSKEY queries if DNSSEC is requested
|
||||
if (dnssecRequested && question.type === 'DNSKEY' && question.name === this.options.dnssecZone) {
|
||||
@ -574,40 +580,41 @@ export class DnsServer {
|
||||
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);
|
||||
|
||||
recordsForQuestion.push(dnskeyAnswer);
|
||||
answered = true;
|
||||
continue;
|
||||
} else {
|
||||
// Collect all matching records from handlers
|
||||
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) {
|
||||
// Ensure the answer has ttl and class
|
||||
const dnsAnswer: DnsAnswer = {
|
||||
...answer,
|
||||
ttl: answer.ttl || 300,
|
||||
class: answer.class || 'IN',
|
||||
};
|
||||
recordsForQuestion.push(dnsAnswer);
|
||||
answered = true;
|
||||
// Continue processing other handlers to allow multiple records
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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;
|
||||
// Continue processing other handlers to allow multiple records
|
||||
}
|
||||
// Add records to response and group by type for DNSSEC
|
||||
if (recordsForQuestion.length > 0) {
|
||||
for (const record of recordsForQuestion) {
|
||||
response.answers.push(record as plugins.dnsPacket.Answer);
|
||||
}
|
||||
|
||||
// Group records by type for DNSSEC signing
|
||||
if (dnssecRequested) {
|
||||
const rrsetKey = `${question.name}:${question.type}`;
|
||||
rrsetMap.set(rrsetKey, recordsForQuestion);
|
||||
}
|
||||
}
|
||||
|
||||
@ -620,7 +627,7 @@ export class DnsServer {
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: {
|
||||
mname: `ns1.${this.options.dnssecZone}`,
|
||||
mname: this.options.primaryNameserver || `ns1.${this.options.dnssecZone}`,
|
||||
rname: `hostmaster.${this.options.dnssecZone}`,
|
||||
serial: Math.floor(Date.now() / 1000),
|
||||
refresh: 3600,
|
||||
@ -633,6 +640,16 @@ export class DnsServer {
|
||||
}
|
||||
}
|
||||
|
||||
// Sign RRsets if DNSSEC is requested
|
||||
if (dnssecRequested) {
|
||||
for (const [key, rrset] of rrsetMap) {
|
||||
const [name, type] = key.split(':');
|
||||
// Sign the entire RRset together
|
||||
const rrsig = this.generateRRSIG(type, rrset, name);
|
||||
response.answers.push(rrsig as plugins.dnsPacket.Answer);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -760,9 +777,21 @@ export class DnsServer {
|
||||
// NS records contain domain names
|
||||
return this.nameToBuffer(data);
|
||||
case 'SOA':
|
||||
// Implement SOA record serialization if needed
|
||||
// For now, return an empty buffer or handle as needed
|
||||
return Buffer.alloc(0);
|
||||
// Implement SOA record serialization according to RFC 1035
|
||||
const mname = this.nameToBuffer(data.mname);
|
||||
const rname = this.nameToBuffer(data.rname);
|
||||
const serial = Buffer.alloc(4);
|
||||
serial.writeUInt32BE(data.serial, 0);
|
||||
const refresh = Buffer.alloc(4);
|
||||
refresh.writeUInt32BE(data.refresh, 0);
|
||||
const retry = Buffer.alloc(4);
|
||||
retry.writeUInt32BE(data.retry, 0);
|
||||
const expire = Buffer.alloc(4);
|
||||
expire.writeUInt32BE(data.expire, 0);
|
||||
const minimum = Buffer.alloc(4);
|
||||
minimum.writeUInt32BE(data.minimum, 0);
|
||||
|
||||
return Buffer.concat([mname, rname, serial, refresh, retry, expire, minimum]);
|
||||
// Add cases for other record types as needed
|
||||
default:
|
||||
throw new Error(`Serialization for record type ${type} is not implemented.`);
|
||||
|
Reference in New Issue
Block a user