Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
abbb971d6a | |||
911a20c86d | |||
1b9eefd70f | |||
f29962a6dc | |||
afd1c18496 | |||
0ea622aa8d |
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
68
.serena/project.yml
Normal file
68
.serena/project.yml
Normal 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"
|
25
changelog.md
25
changelog.md
@@ -1,5 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-09-12 - 7.6.1 - fix(classes.dnsclient)
|
||||
Remove redundant DOH response parsing in getRecords to avoid duplicate processing and clean up client code
|
||||
|
||||
- Removed a duplicated/extra iteration that parsed DNS-over-HTTPS (DoH) answers in ts_client/classes.dnsclient.ts.
|
||||
- Prevents double-processing or incorrect return behavior from Smartdns.getRecords when using DoH providers.
|
||||
- Changes affect the Smartdns client implementation (ts_client/classes.dnsclient.ts).
|
||||
|
||||
## 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)
|
||||
Add MX record DNSSEC support for proper serialization and authentication of mail exchange records
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartdns",
|
||||
"version": "7.5.0",
|
||||
"version": "7.6.1",
|
||||
"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.",
|
||||
"exports": {
|
||||
@@ -44,7 +44,7 @@
|
||||
"homepage": "https://code.foss.global/push.rocks/smartdns",
|
||||
"dependencies": {
|
||||
"@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/smartrequest": "^2.1.0",
|
||||
"@tsclass/tsclass": "^9.2.0",
|
||||
@@ -56,9 +56,9 @@
|
||||
"minimatch": "^10.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.6.4",
|
||||
"@git.zone/tsbuild": "^2.6.8",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^2.3.1",
|
||||
"@git.zone/tstest": "^2.3.7",
|
||||
"@types/node": "^22.15.21"
|
||||
},
|
||||
"files": [
|
||||
|
4613
pnpm-lock.yaml
generated
4613
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- mongodb-memory-server
|
||||
- puppeteer
|
@@ -219,7 +219,7 @@ tap.test('Default primary nameserver with FQDN', async () => {
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('✅ FQDN primary nameserver:', soaData.mname);
|
||||
|
||||
expect(soaData.mname).toEqual('ns.example.com.');
|
||||
expect(soaData.mname).toEqual('ns.example.com');
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
|
@@ -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 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('NS records:', dnsResponse.answers.map(a => (a as any).data));
|
||||
|
||||
// CURRENT BEHAVIOR: Only returns 1 NS record due to the break statement
|
||||
expect(dnsResponse.answers.length).toEqual(1);
|
||||
expect((dnsResponse.answers[0] as any).data).toEqual('ns1.example.com');
|
||||
// Should return all registered NS records
|
||||
expect(dnsResponse.answers.length).toEqual(2);
|
||||
const nsData = dnsResponse.answers.map(a => (a as any).data).sort();
|
||||
expect(nsData).toEqual(['ns1.example.com', 'ns2.example.com']);
|
||||
|
||||
await stopServer(dnsServer);
|
||||
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 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('A records:', dnsResponse.answers.map(a => (a as any).data));
|
||||
|
||||
// CURRENT BEHAVIOR: Only returns 1 A record, preventing round-robin DNS
|
||||
expect(dnsResponse.answers.length).toEqual(1);
|
||||
expect((dnsResponse.answers[0] as any).data).toEqual('10.0.0.1');
|
||||
// Should return all registered A records for round-robin DNS
|
||||
expect(dnsResponse.answers.length).toEqual(3);
|
||||
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);
|
||||
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 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('TXT records:', dnsResponse.answers.map(a => (a as any).data));
|
||||
|
||||
// CURRENT BEHAVIOR: Only returns 1 TXT record instead of all 3
|
||||
expect(dnsResponse.answers.length).toEqual(1);
|
||||
expect((dnsResponse.answers[0] as any).data[0]).toInclude('spf1');
|
||||
// Should return all registered TXT records
|
||||
expect(dnsResponse.answers.length).toEqual(3);
|
||||
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);
|
||||
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 udpPort = getUniqueUdpPort();
|
||||
|
||||
@@ -343,11 +348,11 @@ tap.test('should show the current workaround pattern', async () => {
|
||||
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'];
|
||||
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) => {
|
||||
const record = nsRecords[nsIndex % nsRecords.length];
|
||||
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('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(response2.answers.length).toEqual(1);
|
||||
expect((response1.answers[0] as any).data).toEqual('ns1.example.com');
|
||||
|
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartdns',
|
||||
version: '7.5.0',
|
||||
version: '7.6.1',
|
||||
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.'
|
||||
}
|
||||
|
@@ -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 {
|
||||
Status: number;
|
||||
@@ -43,6 +49,9 @@ export interface IDnsJsonResponse {
|
||||
export class Smartdns {
|
||||
public dnsServerIp: string;
|
||||
public dnsServerPort: number;
|
||||
private strategy: TResolutionStrategy = 'prefer-system';
|
||||
private allowDohFallback = true;
|
||||
private timeoutMs: number | undefined;
|
||||
|
||||
public dnsTypeMap: { [key: string]: number } = {
|
||||
A: 1,
|
||||
@@ -55,7 +64,12 @@ export class Smartdns {
|
||||
/**
|
||||
* 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
|
||||
@@ -133,45 +147,113 @@ export class Smartdns {
|
||||
recordTypeArg: plugins.tsclass.network.TDnsRecordType,
|
||||
retriesCounterArg = 20
|
||||
): 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',
|
||||
},
|
||||
});
|
||||
const responseBody: IDnsJsonResponse = response.body;
|
||||
if (responseBody?.Status !== 0 && counterArg < retriesCounterArg) {
|
||||
await plugins.smartdelay.delayFor(500);
|
||||
return getResponseBody(counterArg + 1);
|
||||
} else {
|
||||
return responseBody;
|
||||
const trySystem = async (): Promise<plugins.tsclass.network.IDnsRecord[]> => {
|
||||
// Prefer dns.lookup for A/AAAA so hosts file and OS resolver are honored
|
||||
if (recordTypeArg === 'A' || recordTypeArg === 'AAAA') {
|
||||
const family = recordTypeArg === 'A' ? 4 : 6;
|
||||
const addresses = await new Promise<{ address: string }[]>((resolve, reject) => {
|
||||
const timer = this.timeoutMs
|
||||
? setTimeout(() => reject(new Error('system lookup timeout')), this.timeoutMs)
|
||||
: null;
|
||||
plugins.dns.lookup(
|
||||
recordNameArg,
|
||||
{ family, all: true },
|
||||
(err, result) => {
|
||||
if (timer) clearTimeout(timer as any);
|
||||
if (err) return reject(err);
|
||||
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;
|
||||
}
|
||||
for (const dnsEntry of responseBody.Answer) {
|
||||
if (dnsEntry.data.startsWith('"') && dnsEntry.data.endsWith('"')) {
|
||||
dnsEntry.data = dnsEntry.data.replace(/^"(.*)"$/, '$1');
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.strategy === 'system') {
|
||||
return await trySystem();
|
||||
}
|
||||
if (dnsEntry.name.endsWith('.')) {
|
||||
dnsEntry.name = dnsEntry.name.substring(0, dnsEntry.name.length - 1);
|
||||
if (this.strategy === 'doh') {
|
||||
return await tryDoh();
|
||||
}
|
||||
returnArray.push({
|
||||
name: dnsEntry.name,
|
||||
type: this.convertDnsTypeNumberToTypeName(dnsEntry.type),
|
||||
dnsSecEnabled: responseBody.AD,
|
||||
value: dnsEntry.data,
|
||||
});
|
||||
// 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 [];
|
||||
}
|
||||
// console.log(responseBody);
|
||||
return returnArray;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* gets a record using nodejs dns resolver
|
||||
*/
|
||||
|
@@ -15,6 +15,8 @@ export interface IDnsServerOptions {
|
||||
manualHttpsMode?: boolean;
|
||||
// Primary nameserver for SOA records (defaults to ns1.{dnssecZone})
|
||||
primaryNameserver?: string;
|
||||
// Local handling for RFC 6761 localhost (default: true)
|
||||
enableLocalhostHandling?: boolean;
|
||||
}
|
||||
|
||||
export interface DnsAnswer {
|
||||
@@ -569,6 +571,7 @@ export class DnsServer {
|
||||
console.log(`Query for ${question.name} of type ${question.type}`);
|
||||
|
||||
let answered = false;
|
||||
let shouldSignRrset = true; // skip DNSSEC signing for synthetic/local answers
|
||||
const recordsForQuestion: DnsAnswer[] = [];
|
||||
|
||||
// Handle DNSKEY queries if DNSSEC is requested
|
||||
@@ -582,9 +585,59 @@ export class DnsServer {
|
||||
};
|
||||
recordsForQuestion.push(dnskeyAnswer);
|
||||
answered = true;
|
||||
// DNSKEY is signable, keep shouldSignRrset true
|
||||
} 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
|
||||
for (const handlerEntry of this.handlers) {
|
||||
if (!answered) {
|
||||
for (const handlerEntry of this.handlers) {
|
||||
if (
|
||||
plugins.minimatch.minimatch(question.name, handlerEntry.domainPattern) &&
|
||||
handlerEntry.recordTypes.includes(question.type)
|
||||
@@ -602,6 +655,7 @@ export class DnsServer {
|
||||
// Continue processing other handlers to allow multiple records
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,7 +666,7 @@ export class DnsServer {
|
||||
}
|
||||
|
||||
// Group records by type for DNSSEC signing
|
||||
if (dnssecRequested) {
|
||||
if (dnssecRequested && shouldSignRrset) {
|
||||
const rrsetKey = `${question.name}:${question.type}`;
|
||||
rrsetMap.set(rrsetKey, recordsForQuestion);
|
||||
}
|
||||
@@ -1019,7 +1073,9 @@ export class DnsServer {
|
||||
}
|
||||
|
||||
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 len = Buffer.byteLength(label, 'utf8');
|
||||
const buf = Buffer.alloc(1 + len);
|
||||
@@ -1027,6 +1083,7 @@ export class DnsServer {
|
||||
buf.write(label, 1);
|
||||
return buf;
|
||||
});
|
||||
return Buffer.concat([...buffers, Buffer.from([0])]); // Add root label
|
||||
// Append exactly one root label
|
||||
return Buffer.concat([...buffers, Buffer.from([0])]);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user