Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
abbb971d6a | |||
911a20c86d | |||
1b9eefd70f | |||
f29962a6dc | |||
afd1c18496 | |||
0ea622aa8d | |||
56a33dd7ae | |||
9e5fae055f | |||
afdd6a6074 | |||
3d06131e04 |
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"
|
47
changelog.md
47
changelog.md
@@ -1,5 +1,52 @@
|
||||
# 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
|
||||
|
||||
- Serialize MX records by combining a 16-bit preference with the exchange domain name
|
||||
- Enable DNSSEC signature generation for MX records to authenticate mail exchange data
|
||||
- Update documentation to include the new MX record DNSSEC support in version v7.4.8
|
||||
|
||||
## 2025-05-30 - 7.4.7 - fix(dnsserver)
|
||||
Update documentation to clarify the primaryNameserver option and SOA record behavior in the DNS server. The changes detail how the primaryNameserver configuration customizes the SOA mname, ensures proper DNSSEC signing for RRsets, and updates the configuration interface examples.
|
||||
|
||||
- Documented the primaryNameserver option in IDnsServerOptions with default behavior (ns1.{dnssecZone})
|
||||
- Clarified SOA record generation including mname, rname, serial, and TTL fields
|
||||
- Updated readme examples to demonstrate binding interfaces and proper DNS server configuration
|
||||
|
||||
## 2025-05-30 - 7.4.6 - docs(readme)
|
||||
Document the primaryNameserver option and SOA record behavior in the DNS server documentation.
|
||||
|
||||
- Added comprehensive documentation for the primaryNameserver option in IDnsServerOptions
|
||||
- Explained SOA record automatic generation and the role of the primary nameserver
|
||||
- Clarified that only one nameserver is designated as primary in SOA records
|
||||
- Updated the configuration options interface documentation with all available options
|
||||
|
||||
## 2025-05-30 - 7.4.3 - fix(dnsserver)
|
||||
Fix DNSSEC RRset signing, SOA record timeout issues, and add configurable primary nameserver support.
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartdns",
|
||||
"version": "7.4.6",
|
||||
"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
|
@@ -110,6 +110,13 @@ The test suite demonstrates:
|
||||
2. **SOA Record Serialization**: Implemented proper SOA record encoding for DNSSEC compatibility
|
||||
3. **Configurable Primary Nameserver**: Added `primaryNameserver` option to customize SOA mname field
|
||||
|
||||
## Recent Improvements (v7.4.8)
|
||||
|
||||
1. **MX Record DNSSEC Support**: Implemented MX record serialization for DNSSEC signing
|
||||
- MX records consist of a 16-bit preference value followed by the exchange domain name
|
||||
- Properly serializes both components for DNSSEC signature generation
|
||||
- Enables mail exchange records to be authenticated with DNSSEC
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Handler Deduplication**: If the same handler is registered multiple times, it will contribute duplicate records (this may be desired behavior for some use cases)
|
45
readme.md
45
readme.md
@@ -198,7 +198,8 @@ const secureServer = new DnsServer({
|
||||
httpsCert: 'path/to/cert.pem',
|
||||
dnssecZone: 'example.com',
|
||||
udpBindInterface: '127.0.0.1', // Bind UDP to localhost only
|
||||
httpsBindInterface: '127.0.0.1' // Bind HTTPS to localhost only
|
||||
httpsBindInterface: '127.0.0.1', // Bind HTTPS to localhost only
|
||||
primaryNameserver: 'ns1.example.com' // Optional: primary nameserver for SOA records (defaults to ns1.{dnssecZone})
|
||||
});
|
||||
|
||||
// Register a handler for all subdomains of example.com
|
||||
@@ -224,6 +225,35 @@ await dnsServer.start();
|
||||
console.log('DNS Server started!');
|
||||
```
|
||||
|
||||
### SOA Records and Primary Nameserver
|
||||
|
||||
The DNS server automatically generates SOA (Start of Authority) records for zones when no specific handler matches a query. The SOA record contains important zone metadata including the primary nameserver.
|
||||
|
||||
```typescript
|
||||
const dnsServer = new DnsServer({
|
||||
udpPort: 53,
|
||||
httpsPort: 443,
|
||||
httpsKey: 'path/to/key.pem',
|
||||
httpsCert: 'path/to/cert.pem',
|
||||
dnssecZone: 'example.com',
|
||||
primaryNameserver: 'ns1.example.com' // Specify your actual primary nameserver
|
||||
});
|
||||
|
||||
// Without primaryNameserver, the SOA mname defaults to 'ns1.{dnssecZone}'
|
||||
// In this case, it would be 'ns1.example.com'
|
||||
|
||||
// The automatic SOA record includes:
|
||||
// - mname: Primary nameserver (from primaryNameserver option)
|
||||
// - rname: Responsible person email (hostmaster.{dnssecZone})
|
||||
// - serial: Unix timestamp
|
||||
// - refresh: 3600 (1 hour)
|
||||
// - retry: 600 (10 minutes)
|
||||
// - expire: 604800 (7 days)
|
||||
// - minimum: 86400 (1 day)
|
||||
```
|
||||
|
||||
**Important**: Even if you have multiple nameservers (NS records), only one is designated as the primary in the SOA record. All authoritative nameservers should return the same SOA record.
|
||||
|
||||
### DNSSEC Support
|
||||
|
||||
The DNS server includes comprehensive DNSSEC support with automatic key generation and record signing:
|
||||
@@ -314,9 +344,16 @@ The DNS server supports manual socket handling for advanced use cases like clust
|
||||
|
||||
```typescript
|
||||
export interface IDnsServerOptions {
|
||||
// ... standard options ...
|
||||
manualUdpMode?: boolean; // Handle UDP sockets manually
|
||||
manualHttpsMode?: boolean; // Handle HTTPS sockets manually
|
||||
httpsKey: string; // Path or content of HTTPS private key
|
||||
httpsCert: string; // Path or content of HTTPS certificate
|
||||
httpsPort: number; // Port for DNS-over-HTTPS
|
||||
udpPort: number; // Port for standard UDP DNS
|
||||
dnssecZone: string; // Zone name for DNSSEC signing
|
||||
udpBindInterface?: string; // IP address to bind UDP socket (default: '0.0.0.0')
|
||||
httpsBindInterface?: string; // IP address to bind HTTPS server (default: '0.0.0.0')
|
||||
manualUdpMode?: boolean; // Handle UDP sockets manually
|
||||
manualHttpsMode?: boolean; // Handle HTTPS sockets manually
|
||||
primaryNameserver?: string; // Primary nameserver for SOA records (default: 'ns1.{dnssecZone}')
|
||||
}
|
||||
```
|
||||
|
||||
|
165
readme.plan.md
165
readme.plan.md
@@ -1,165 +0,0 @@
|
||||
# SmartDNS Improvement Plan
|
||||
|
||||
Command to reread CLAUDE.md: `cat /home/philkunz/.claude/CLAUDE.md`
|
||||
|
||||
## Critical Issue: Support Multiple DNS Records of Same Type
|
||||
|
||||
### Current Status: ✅ IMPLEMENTED (v7.4.2)
|
||||
**Priority: HIGH** - This issue blocks proper DNS server operation and domain registration
|
||||
|
||||
## All Issues Fixed (v7.4.3)
|
||||
|
||||
### Successfully Implemented:
|
||||
1. ✅ **Multiple DNS Records Support** (v7.4.2) - Core fix allowing multiple handlers to contribute records
|
||||
2. ✅ **DNSSEC RRset Signing** - Now signs entire RRsets together instead of individual records
|
||||
3. ✅ **SOA Record Serialization** - Proper SOA record encoding for DNSSEC compatibility
|
||||
4. ✅ **Configurable Primary Nameserver** - Added `primaryNameserver` option to IDnsServerOptions
|
||||
|
||||
### Problem Summary
|
||||
The DNS server currently exits after finding the first matching handler for a query, preventing it from serving multiple records of the same type (e.g., multiple NS records, multiple A records for round-robin, multiple TXT records).
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
#### Phase 1: Analysis and Testing ✅ COMPLETED
|
||||
- [x] Create comprehensive test cases demonstrating the issue
|
||||
- [x] Test with multiple NS records scenario
|
||||
- [x] Test with multiple A records (round-robin) scenario
|
||||
- [x] Test with multiple TXT records scenario
|
||||
- [x] Document current behavior vs expected behavior
|
||||
|
||||
#### Phase 2: Core Fix Implementation ✅ COMPLETED
|
||||
- [x] Remove the `break` statement in `processDnsRequest` method (line 609)
|
||||
- [x] Ensure all matching handlers are processed
|
||||
- [x] Accumulate all answers from matching handlers
|
||||
- [x] Add NS record serialization for DNSSEC support
|
||||
|
||||
#### Phase 3: Handler Interface Enhancement (Optional)
|
||||
- [ ] Consider allowing handlers to return arrays of records
|
||||
- [ ] Update `IDnsHandler` interface to support `DnsAnswer | DnsAnswer[] | null`
|
||||
- [ ] Update processing logic to handle array responses
|
||||
- [ ] Maintain backward compatibility with existing handlers
|
||||
|
||||
#### Phase 4: Testing and Validation
|
||||
- [ ] Test multiple NS records return correctly
|
||||
- [ ] Test round-robin DNS with multiple A records
|
||||
- [ ] Test multiple TXT records (SPF + DKIM + verification)
|
||||
- [ ] Test DNSSEC signatures for multiple records
|
||||
- [ ] Verify no regression in single-record scenarios
|
||||
|
||||
#### Phase 5: Documentation and Examples
|
||||
- [ ] Update documentation with multiple record examples
|
||||
- [ ] Add example for registering multiple NS records
|
||||
- [ ] Add example for round-robin DNS setup
|
||||
- [ ] Document best practices for handler registration
|
||||
|
||||
### Technical Details
|
||||
|
||||
#### Current Code Issue (ts_server/classes.dnsserver.ts:609)
|
||||
```typescript
|
||||
answered = true;
|
||||
break; // <-- This prevents multiple handlers from contributing answers
|
||||
```
|
||||
|
||||
#### Proposed Fix
|
||||
```typescript
|
||||
answered = true;
|
||||
// Continue processing other handlers instead of breaking
|
||||
```
|
||||
|
||||
### Success Criteria
|
||||
- DNS queries return ALL matching records from ALL matching handlers
|
||||
- Domain registration with multiple NS records succeeds
|
||||
- Round-robin DNS works with multiple A records
|
||||
- Multiple TXT records can be served for the same domain
|
||||
- DNSSEC signatures are properly generated for all returned records
|
||||
|
||||
### Implementation Summary
|
||||
|
||||
#### What Was Fixed
|
||||
1. **Core Issue Resolved**: Removed the `break` statement at line 609 in `processDnsRequest` that was preventing multiple handlers from contributing DNS answers
|
||||
2. **NS Record Serialization**: Added NS record type support in `serializeRData` method for DNSSEC compatibility
|
||||
3. **Result**: DNS server now correctly returns multiple records of the same type from different handlers
|
||||
|
||||
#### Test Results
|
||||
- ✅ Multiple NS records now work (2+ nameservers returned)
|
||||
- ✅ Round-robin DNS with multiple A records works
|
||||
- ✅ Multiple TXT records (SPF, DKIM, verification) work
|
||||
- ⚠️ DNSSEC RRSIG generation needs additional fixes for multiple record scenarios
|
||||
|
||||
#### Code Changes
|
||||
```typescript
|
||||
// Before (line 609):
|
||||
answered = true;
|
||||
break; // This was preventing multiple handlers from running
|
||||
|
||||
// After:
|
||||
answered = true;
|
||||
// Continue processing other handlers to allow multiple records
|
||||
```
|
||||
|
||||
## Next Steps and Future Improvements
|
||||
|
||||
### Released in v7.4.2
|
||||
The critical issue of supporting multiple DNS records of the same type has been successfully implemented and released in version 7.4.2.
|
||||
|
||||
## Comprehensive Fix Plan for Remaining Issues
|
||||
|
||||
Command to reread CLAUDE.md: `cat /home/philkunz/.claude/CLAUDE.md`
|
||||
|
||||
### Outstanding Issues to Address
|
||||
|
||||
#### 1. DNSSEC RRSIG Generation for Multiple Records
|
||||
**Status**: Pending
|
||||
**Priority**: Medium
|
||||
**Issue**: When multiple records of the same type are returned with DNSSEC enabled, the RRSIG generation may encounter issues with the current implementation. Each record gets its own RRSIG instead of signing the entire RRset together.
|
||||
|
||||
**Implementation Plan**:
|
||||
1. Modify `processDnsRequest` to collect all records of the same type before signing
|
||||
2. Create a map to group answers by record type
|
||||
3. After all handlers have been processed, sign each RRset as a whole
|
||||
4. Generate one RRSIG per record type (not per record)
|
||||
5. Update tests to verify proper DNSSEC RRset signing
|
||||
6. Ensure canonical ordering of records in RRset for consistent signatures
|
||||
|
||||
**Code Changes**:
|
||||
- Refactor the DNSSEC signing logic in `processDnsRequest`
|
||||
- Move RRSIG generation outside the handler loop
|
||||
- Group records by type before signing
|
||||
|
||||
#### 2. SOA Record Timeout Issues
|
||||
**Status**: Not Started
|
||||
**Priority**: Low
|
||||
**Issue**: SOA queries sometimes timeout or return incorrect data, possibly related to incomplete SOA record serialization.
|
||||
|
||||
**Implementation Plan**:
|
||||
1. Implement proper SOA record serialization in `serializeRData` method
|
||||
2. Ensure all SOA fields are properly encoded in wire format
|
||||
3. Add comprehensive SOA record tests
|
||||
4. Verify SOA responses with standard DNS tools (dig, nslookup)
|
||||
|
||||
**Code Changes**:
|
||||
- Implement SOA serialization in `serializeRData` method
|
||||
- Add SOA-specific test cases
|
||||
|
||||
#### 3. Configurable DNSSEC Zone Prefix
|
||||
**Status**: Not Started
|
||||
**Priority**: Low
|
||||
**Issue**: The server hardcodes 'ns1.' prefix for SOA mname field which may not match actual nameserver names.
|
||||
|
||||
**Implementation Plan**:
|
||||
1. Add `primaryNameserver` option to `IDnsServerOptions`
|
||||
2. Default to `ns1.{dnssecZone}` if not provided
|
||||
3. Update SOA record generation to use configurable nameserver
|
||||
4. Update documentation with new option
|
||||
5. Add tests for custom primary nameserver configuration
|
||||
|
||||
**Code Changes**:
|
||||
- Add `primaryNameserver?: string` to `IDnsServerOptions`
|
||||
- Update SOA mname field generation logic
|
||||
- Update constructor to handle the new option
|
||||
|
||||
### Testing Recommendations
|
||||
- Test DNSSEC validation with multiple records using `dig +dnssec`
|
||||
- Verify SOA records with `dig SOA`
|
||||
- Test custom nameserver configuration
|
||||
- Validate with real-world DNS resolvers (Google DNS, Cloudflare)
|
@@ -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.4.2',
|
||||
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);
|
||||
}
|
||||
@@ -809,6 +863,12 @@ export class DnsServer {
|
||||
minimum.writeUInt32BE(data.minimum, 0);
|
||||
|
||||
return Buffer.concat([mname, rname, serial, refresh, retry, expire, minimum]);
|
||||
case 'MX':
|
||||
// MX records contain preference (16-bit) and exchange (domain name)
|
||||
const preference = Buffer.alloc(2);
|
||||
preference.writeUInt16BE(data.preference, 0);
|
||||
const exchange = this.nameToBuffer(data.exchange);
|
||||
return Buffer.concat([preference, exchange]);
|
||||
// Add cases for other record types as needed
|
||||
default:
|
||||
throw new Error(`Serialization for record type ${type} is not implemented.`);
|
||||
@@ -1013,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);
|
||||
@@ -1021,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