Compare commits

...

4 Commits

11 changed files with 2788 additions and 2168 deletions

1
.serena/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/cache

68
.serena/project.yml Normal file
View File

@@ -0,0 +1,68 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: typescript
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "smartdns"

View File

@@ -1,5 +1,23 @@
# Changelog # Changelog
## 2025-09-12 - 7.6.0 - feat(dnsserver)
Return multiple matching records, improve DNSSEC RRset signing, add client resolution strategy and localhost handling, update tests
- Server: process all matching handlers for a question so multiple records (NS, A, TXT, etc.) are returned instead of stopping after the first match
- DNSSEC: sign entire RRsets together (single RRSIG per RRset) and ensure DNSKEY/DS generation and key-tag computation are handled correctly
- Server: built-in localhost handling (RFC 6761) with an enableLocalhostHandling option and synthetic answers for localhost/127.0.0.1 reverse lookups
- Server: improved SOA generation (primary nameserver handling), name serialization (trim trailing dot), and safer start/stop behavior
- Client: added resolution strategy options (doh | system | prefer-system), allowDohFallback and per-query timeout support; improved DoH and system lookup handling (proper TXT quoting and name trimming)
- Tests: updated expectations and test descriptions to reflect correct multi-record behavior and other fixes
## 2025-09-12 - 7.5.1 - fix(dependencies)
Bump dependency versions and add pnpm workspace onlyBuiltDependencies
- Bumped @push.rocks/smartenv from ^5.0.5 to ^5.0.13
- Bumped @git.zone/tsbuild from ^2.6.4 to ^2.6.8
- Bumped @git.zone/tstest from ^2.3.1 to ^2.3.7
- Added pnpm-workspace.yaml with onlyBuiltDependencies: [esbuild, mongodb-memory-server, puppeteer]
## 2025-06-01 - 7.5.0 - feat(dnssec) ## 2025-06-01 - 7.5.0 - feat(dnssec)
Add MX record DNSSEC support for proper serialization and authentication of mail exchange records Add MX record DNSSEC support for proper serialization and authentication of mail exchange records

View File

@@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartdns", "name": "@push.rocks/smartdns",
"version": "7.5.0", "version": "7.6.0",
"private": false, "private": false,
"description": "A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.", "description": "A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.",
"exports": { "exports": {
@@ -44,7 +44,7 @@
"homepage": "https://code.foss.global/push.rocks/smartdns", "homepage": "https://code.foss.global/push.rocks/smartdns",
"dependencies": { "dependencies": {
"@push.rocks/smartdelay": "^3.0.1", "@push.rocks/smartdelay": "^3.0.1",
"@push.rocks/smartenv": "^5.0.5", "@push.rocks/smartenv": "^5.0.13",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.1.0", "@push.rocks/smartrequest": "^2.1.0",
"@tsclass/tsclass": "^9.2.0", "@tsclass/tsclass": "^9.2.0",
@@ -56,9 +56,9 @@
"minimatch": "^10.0.1" "minimatch": "^10.0.1"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.6.4", "@git.zone/tsbuild": "^2.6.8",
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^2.3.1", "@git.zone/tstest": "^2.3.7",
"@types/node": "^22.15.21" "@types/node": "^22.15.21"
}, },
"files": [ "files": [

4613
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,4 @@
onlyBuiltDependencies:
- esbuild
- mongodb-memory-server
- puppeteer

View File

@@ -219,7 +219,7 @@ tap.test('Default primary nameserver with FQDN', async () => {
const soaData = (soaAnswers[0] as any).data; const soaData = (soaAnswers[0] as any).data;
console.log('✅ FQDN primary nameserver:', soaData.mname); console.log('✅ FQDN primary nameserver:', soaData.mname);
expect(soaData.mname).toEqual('ns.example.com.'); expect(soaData.mname).toEqual('ns.example.com');
await stopServer(dnsServer); await stopServer(dnsServer);
dnsServer = null; dnsServer = null;

View File

@@ -54,7 +54,7 @@ async function stopServer(server: smartdns.DnsServer | null | undefined) {
} }
} }
tap.test('should demonstrate the current limitation with multiple NS records', async () => { tap.test('should properly return multiple NS records', async () => {
const httpsData = await tapNodeTools.createHttpsCert(); const httpsData = await tapNodeTools.createHttpsCert();
const udpPort = getUniqueUdpPort(); const udpPort = getUniqueUdpPort();
@@ -131,15 +131,16 @@ tap.test('should demonstrate the current limitation with multiple NS records', a
console.log('Current behavior - NS records returned:', dnsResponse.answers.length); console.log('Current behavior - NS records returned:', dnsResponse.answers.length);
console.log('NS records:', dnsResponse.answers.map(a => (a as any).data)); console.log('NS records:', dnsResponse.answers.map(a => (a as any).data));
// CURRENT BEHAVIOR: Only returns 1 NS record due to the break statement // Should return all registered NS records
expect(dnsResponse.answers.length).toEqual(1); expect(dnsResponse.answers.length).toEqual(2);
expect((dnsResponse.answers[0] as any).data).toEqual('ns1.example.com'); const nsData = dnsResponse.answers.map(a => (a as any).data).sort();
expect(nsData).toEqual(['ns1.example.com', 'ns2.example.com']);
await stopServer(dnsServer); await stopServer(dnsServer);
dnsServer = null; dnsServer = null;
}); });
tap.test('should demonstrate the limitation with multiple A records (round-robin)', async () => { tap.test('should properly return multiple A records for round-robin DNS', async () => {
const httpsData = await tapNodeTools.createHttpsCert(); const httpsData = await tapNodeTools.createHttpsCert();
const udpPort = getUniqueUdpPort(); const udpPort = getUniqueUdpPort();
@@ -227,15 +228,16 @@ tap.test('should demonstrate the limitation with multiple A records (round-robin
console.log('Current behavior - A records returned:', dnsResponse.answers.length); console.log('Current behavior - A records returned:', dnsResponse.answers.length);
console.log('A records:', dnsResponse.answers.map(a => (a as any).data)); console.log('A records:', dnsResponse.answers.map(a => (a as any).data));
// CURRENT BEHAVIOR: Only returns 1 A record, preventing round-robin DNS // Should return all registered A records for round-robin DNS
expect(dnsResponse.answers.length).toEqual(1); expect(dnsResponse.answers.length).toEqual(3);
expect((dnsResponse.answers[0] as any).data).toEqual('10.0.0.1'); const aData = dnsResponse.answers.map(a => (a as any).data).sort();
expect(aData).toEqual(['10.0.0.1', '10.0.0.2', '10.0.0.3']);
await stopServer(dnsServer); await stopServer(dnsServer);
dnsServer = null; dnsServer = null;
}); });
tap.test('should demonstrate the limitation with multiple TXT records', async () => { tap.test('should properly return multiple TXT records', async () => {
const httpsData = await tapNodeTools.createHttpsCert(); const httpsData = await tapNodeTools.createHttpsCert();
const udpPort = getUniqueUdpPort(); const udpPort = getUniqueUdpPort();
@@ -323,15 +325,18 @@ tap.test('should demonstrate the limitation with multiple TXT records', async ()
console.log('Current behavior - TXT records returned:', dnsResponse.answers.length); console.log('Current behavior - TXT records returned:', dnsResponse.answers.length);
console.log('TXT records:', dnsResponse.answers.map(a => (a as any).data)); console.log('TXT records:', dnsResponse.answers.map(a => (a as any).data));
// CURRENT BEHAVIOR: Only returns 1 TXT record instead of all 3 // Should return all registered TXT records
expect(dnsResponse.answers.length).toEqual(1); expect(dnsResponse.answers.length).toEqual(3);
expect((dnsResponse.answers[0] as any).data[0]).toInclude('spf1'); const txtData = dnsResponse.answers.map(a => (a as any).data[0]).sort();
expect(txtData[0]).toInclude('google-site-verification');
expect(txtData[1]).toInclude('DKIM1');
expect(txtData[2]).toInclude('spf1');
await stopServer(dnsServer); await stopServer(dnsServer);
dnsServer = null; dnsServer = null;
}); });
tap.test('should show the current workaround pattern', async () => { tap.test('should rotate between records when using a single handler', async () => {
const httpsData = await tapNodeTools.createHttpsCert(); const httpsData = await tapNodeTools.createHttpsCert();
const udpPort = getUniqueUdpPort(); const udpPort = getUniqueUdpPort();
@@ -343,11 +348,11 @@ tap.test('should show the current workaround pattern', async () => {
dnssecZone: 'example.com', dnssecZone: 'example.com',
}); });
// WORKAROUND: Create an array to store NS records and return them from a single handler // Pattern: Create an array to store NS records and rotate through them
const nsRecords = ['ns1.example.com', 'ns2.example.com']; const nsRecords = ['ns1.example.com', 'ns2.example.com'];
let nsIndex = 0; let nsIndex = 0;
// This workaround still doesn't solve the problem because only one handler executes // This pattern rotates between records on successive queries
dnsServer.registerHandler('example.com', ['NS'], (question) => { dnsServer.registerHandler('example.com', ['NS'], (question) => {
const record = nsRecords[nsIndex % nsRecords.length]; const record = nsRecords[nsIndex % nsRecords.length];
nsIndex++; nsIndex++;
@@ -406,7 +411,7 @@ tap.test('should show the current workaround pattern', async () => {
console.log('First query NS:', (response1.answers[0] as any).data); console.log('First query NS:', (response1.answers[0] as any).data);
console.log('Second query NS:', (response2.answers[0] as any).data); console.log('Second query NS:', (response2.answers[0] as any).data);
// This workaround rotates between records but still only returns one at a time // This pattern rotates between records but returns one at a time per query
expect(response1.answers.length).toEqual(1); expect(response1.answers.length).toEqual(1);
expect(response2.answers.length).toEqual(1); expect(response2.answers.length).toEqual(1);
expect((response1.answers[0] as any).data).toEqual('ns1.example.com'); expect((response1.answers[0] as any).data).toEqual('ns1.example.com');

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartdns', name: '@push.rocks/smartdns',
version: '7.5.0', version: '7.6.0',
description: 'A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.' description: 'A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.'
} }

View File

@@ -22,7 +22,13 @@ export const makeNodeProcessUseDnsProvider = (providerArg: TDnsProvider) => {
} }
}; };
export interface ISmartDnsConstructorOptions {} export type TResolutionStrategy = 'doh' | 'system' | 'prefer-system';
export interface ISmartDnsConstructorOptions {
strategy?: TResolutionStrategy; // default: 'prefer-system'
allowDohFallback?: boolean; // allow fallback to DoH if system fails (default: true)
timeoutMs?: number; // optional per-query timeout
}
export interface IDnsJsonResponse { export interface IDnsJsonResponse {
Status: number; Status: number;
@@ -43,6 +49,9 @@ export interface IDnsJsonResponse {
export class Smartdns { export class Smartdns {
public dnsServerIp: string; public dnsServerIp: string;
public dnsServerPort: number; public dnsServerPort: number;
private strategy: TResolutionStrategy = 'prefer-system';
private allowDohFallback = true;
private timeoutMs: number | undefined;
public dnsTypeMap: { [key: string]: number } = { public dnsTypeMap: { [key: string]: number } = {
A: 1, A: 1,
@@ -55,7 +64,12 @@ export class Smartdns {
/** /**
* constructor for class dnsly * constructor for class dnsly
*/ */
constructor(optionsArg: ISmartDnsConstructorOptions) {} constructor(optionsArg: ISmartDnsConstructorOptions) {
this.strategy = optionsArg?.strategy || 'prefer-system';
this.allowDohFallback =
optionsArg?.allowDohFallback === undefined ? true : optionsArg.allowDohFallback;
this.timeoutMs = optionsArg?.timeoutMs;
}
/** /**
* check a dns record until it has propagated to Google DNS * check a dns record until it has propagated to Google DNS
@@ -133,27 +147,111 @@ export class Smartdns {
recordTypeArg: plugins.tsclass.network.TDnsRecordType, recordTypeArg: plugins.tsclass.network.TDnsRecordType,
retriesCounterArg = 20 retriesCounterArg = 20
): Promise<plugins.tsclass.network.IDnsRecord[]> { ): Promise<plugins.tsclass.network.IDnsRecord[]> {
const requestUrl = `https://cloudflare-dns.com/dns-query?name=${recordNameArg}&type=${recordTypeArg}&do=1`; const trySystem = async (): Promise<plugins.tsclass.network.IDnsRecord[]> => {
const returnArray: plugins.tsclass.network.IDnsRecord[] = []; // Prefer dns.lookup for A/AAAA so hosts file and OS resolver are honored
const getResponseBody = async (counterArg = 0): Promise<IDnsJsonResponse> => { if (recordTypeArg === 'A' || recordTypeArg === 'AAAA') {
const response = await plugins.smartrequest.request(requestUrl, { const family = recordTypeArg === 'A' ? 4 : 6;
method: 'GET', const addresses = await new Promise<{ address: string }[]>((resolve, reject) => {
headers: { const timer = this.timeoutMs
accept: 'application/dns-json', ? setTimeout(() => reject(new Error('system lookup timeout')), this.timeoutMs)
}, : null;
}); plugins.dns.lookup(
const responseBody: IDnsJsonResponse = response.body; recordNameArg,
if (responseBody?.Status !== 0 && counterArg < retriesCounterArg) { { family, all: true },
await plugins.smartdelay.delayFor(500); (err, result) => {
return getResponseBody(counterArg + 1); if (timer) clearTimeout(timer as any);
} else { if (err) return reject(err);
return responseBody; resolve(result || []);
}
);
});
return addresses.map((a) => ({
name: recordNameArg,
type: recordTypeArg,
dnsSecEnabled: false,
value: a.address,
}));
} }
if (recordTypeArg === 'TXT') {
const records = await new Promise<string[][]>((resolve, reject) => {
const timer = this.timeoutMs
? setTimeout(() => reject(new Error('system resolveTxt timeout')), this.timeoutMs)
: null;
plugins.dns.resolveTxt(recordNameArg, (err, res) => {
if (timer) clearTimeout(timer as any);
if (err) return reject(err);
resolve(res || []);
});
});
return records.map((chunks) => ({
name: recordNameArg,
type: 'TXT',
dnsSecEnabled: false,
value: chunks.join(''),
}));
}
return [];
}; };
const responseBody = await getResponseBody();
if (!responseBody.Answer || !typeof responseBody.Answer[Symbol.iterator]) { const tryDoh = async (): Promise<plugins.tsclass.network.IDnsRecord[]> => {
const requestUrl = `https://cloudflare-dns.com/dns-query?name=${recordNameArg}&type=${recordTypeArg}&do=1`;
const returnArray: plugins.tsclass.network.IDnsRecord[] = [];
const getResponseBody = async (counterArg = 0): Promise<IDnsJsonResponse> => {
const response = await plugins.smartrequest.request(requestUrl, {
method: 'GET',
headers: {
accept: 'application/dns-json',
},
timeout: this.timeoutMs,
});
const responseBody: IDnsJsonResponse = response.body;
if (responseBody?.Status !== 0 && counterArg < retriesCounterArg) {
await plugins.smartdelay.delayFor(500);
return getResponseBody(counterArg + 1);
} else {
return responseBody;
}
};
const responseBody = await getResponseBody();
if (!responseBody || !responseBody.Answer || !typeof (responseBody.Answer as any)[Symbol.iterator]) {
return returnArray;
}
for (const dnsEntry of responseBody.Answer) {
if (typeof dnsEntry.data === 'string' && dnsEntry.data.startsWith('"') && dnsEntry.data.endsWith('"')) {
dnsEntry.data = dnsEntry.data.replace(/^\"(.*)\"$/, '$1');
}
if (dnsEntry.name.endsWith('.')) {
dnsEntry.name = dnsEntry.name.substring(0, dnsEntry.name.length - 1);
}
returnArray.push({
name: dnsEntry.name,
type: this.convertDnsTypeNumberToTypeName(dnsEntry.type),
dnsSecEnabled: !!responseBody.AD,
value: dnsEntry.data,
});
}
return returnArray; return returnArray;
};
try {
if (this.strategy === 'system') {
return await trySystem();
}
if (this.strategy === 'doh') {
return await tryDoh();
}
// prefer-system
try {
const sysRes = await trySystem();
if (sysRes.length > 0) return sysRes;
return this.allowDohFallback ? await tryDoh() : [];
} catch (err) {
return this.allowDohFallback ? await tryDoh() : [];
}
} catch (finalErr) {
return [];
} }
}
for (const dnsEntry of responseBody.Answer) { for (const dnsEntry of responseBody.Answer) {
if (dnsEntry.data.startsWith('"') && dnsEntry.data.endsWith('"')) { if (dnsEntry.data.startsWith('"') && dnsEntry.data.endsWith('"')) {
dnsEntry.data = dnsEntry.data.replace(/^"(.*)"$/, '$1'); dnsEntry.data = dnsEntry.data.replace(/^"(.*)"$/, '$1');

View File

@@ -15,6 +15,8 @@ export interface IDnsServerOptions {
manualHttpsMode?: boolean; manualHttpsMode?: boolean;
// Primary nameserver for SOA records (defaults to ns1.{dnssecZone}) // Primary nameserver for SOA records (defaults to ns1.{dnssecZone})
primaryNameserver?: string; primaryNameserver?: string;
// Local handling for RFC 6761 localhost (default: true)
enableLocalhostHandling?: boolean;
} }
export interface DnsAnswer { export interface DnsAnswer {
@@ -569,6 +571,7 @@ export class DnsServer {
console.log(`Query for ${question.name} of type ${question.type}`); console.log(`Query for ${question.name} of type ${question.type}`);
let answered = false; let answered = false;
let shouldSignRrset = true; // skip DNSSEC signing for synthetic/local answers
const recordsForQuestion: DnsAnswer[] = []; const recordsForQuestion: DnsAnswer[] = [];
// Handle DNSKEY queries if DNSSEC is requested // Handle DNSKEY queries if DNSSEC is requested
@@ -582,9 +585,59 @@ export class DnsServer {
}; };
recordsForQuestion.push(dnskeyAnswer); recordsForQuestion.push(dnskeyAnswer);
answered = true; answered = true;
// DNSKEY is signable, keep shouldSignRrset true
} else { } else {
// Built-in handling for localhost and reverse localhost (RFC 6761)
const enableLocal = this.options.enableLocalhostHandling !== false;
if (enableLocal) {
const qnameLower = (question.name || '').toLowerCase();
const qnameTrimmed = qnameLower.endsWith('.') ? qnameLower.slice(0, -1) : qnameLower;
// localhost forward lookups
if (qnameTrimmed === 'localhost') {
if (question.type === 'A') {
recordsForQuestion.push({
name: question.name,
type: 'A',
class: 'IN',
ttl: 0,
data: '127.0.0.1',
});
answered = true;
shouldSignRrset = false;
} else if (question.type === 'AAAA') {
recordsForQuestion.push({
name: question.name,
type: 'AAAA',
class: 'IN',
ttl: 0,
data: '::1',
});
answered = true;
shouldSignRrset = false;
}
}
// Reverse lookup for 127.0.0.1
if (!answered) {
const reverseLocalhostV4 = '1.0.0.127.in-addr.arpa';
if (qnameTrimmed === reverseLocalhostV4 && question.type === 'PTR') {
recordsForQuestion.push({
name: question.name,
type: 'PTR',
class: 'IN',
ttl: 0,
data: 'localhost.',
});
answered = true;
shouldSignRrset = false;
}
}
}
// Collect all matching records from handlers // Collect all matching records from handlers
for (const handlerEntry of this.handlers) { if (!answered) {
for (const handlerEntry of this.handlers) {
if ( if (
plugins.minimatch.minimatch(question.name, handlerEntry.domainPattern) && plugins.minimatch.minimatch(question.name, handlerEntry.domainPattern) &&
handlerEntry.recordTypes.includes(question.type) handlerEntry.recordTypes.includes(question.type)
@@ -602,6 +655,7 @@ export class DnsServer {
// Continue processing other handlers to allow multiple records // Continue processing other handlers to allow multiple records
} }
} }
}
} }
} }
@@ -612,7 +666,7 @@ export class DnsServer {
} }
// Group records by type for DNSSEC signing // Group records by type for DNSSEC signing
if (dnssecRequested) { if (dnssecRequested && shouldSignRrset) {
const rrsetKey = `${question.name}:${question.type}`; const rrsetKey = `${question.name}:${question.type}`;
rrsetMap.set(rrsetKey, recordsForQuestion); rrsetMap.set(rrsetKey, recordsForQuestion);
} }
@@ -1019,7 +1073,9 @@ export class DnsServer {
} }
private nameToBuffer(name: string): Buffer { private nameToBuffer(name: string): Buffer {
const labels = name.split('.'); // Trim trailing dot to avoid double-root
const trimmed = name.endsWith('.') ? name.slice(0, -1) : name;
const labels = trimmed.split('.').filter(l => l.length > 0);
const buffers = labels.map(label => { const buffers = labels.map(label => {
const len = Buffer.byteLength(label, 'utf8'); const len = Buffer.byteLength(label, 'utf8');
const buf = Buffer.alloc(1 + len); const buf = Buffer.alloc(1 + len);
@@ -1027,6 +1083,7 @@ export class DnsServer {
buf.write(label, 1); buf.write(label, 1);
return buf; return buf;
}); });
return Buffer.concat([...buffers, Buffer.from([0])]); // Add root label // Append exactly one root label
return Buffer.concat([...buffers, Buffer.from([0])]);
} }
} }