Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
1b9eefd70f | |||
f29962a6dc | |||
afd1c18496 | |||
0ea622aa8d | |||
56a33dd7ae | |||
9e5fae055f | |||
afdd6a6074 | |||
3d06131e04 | |||
1811ebd4d4 | |||
e7ace9b596 | |||
f6175d1f2b | |||
d67fbc87e2 | |||
b87cbbee5c |
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"
|
49
changelog.md
49
changelog.md
@@ -1,5 +1,54 @@
|
||||
# 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)
|
||||
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.
|
||||
|
||||
- Fixed DNSSEC to sign entire RRsets together instead of individual records (one RRSIG per record type)
|
||||
- Fixed SOA record serialization by implementing proper wire format encoding in serializeRData method
|
||||
- Fixed RRSIG generation by using correct field names (signersName) and types (string typeCovered)
|
||||
- Added configurable primary nameserver via primaryNameserver option in IDnsServerOptions
|
||||
- Enhanced test coverage with comprehensive SOA and DNSSEC test scenarios
|
||||
|
||||
## 2025-05-30 - 7.4.2 - fix(dnsserver)
|
||||
Enable multiple DNS record support by removing the premature break in processDnsRequest. Now the DNS server aggregates answers from all matching handlers for NS, A, and TXT records, and improves NS record serialization for DNSSEC.
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartdns",
|
||||
"version": "7.4.2",
|
||||
"version": "7.6.0",
|
||||
"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
|
@@ -104,8 +104,19 @@ The test suite demonstrates:
|
||||
- Let's Encrypt integration requires proper domain authorization
|
||||
- Handler patterns should be carefully designed to avoid open resolvers
|
||||
|
||||
## Known Issues
|
||||
## Recent Improvements (v7.4.3)
|
||||
|
||||
1. **DNSSEC RRSIG Generation**: When multiple records of the same type are returned, DNSSEC signing may encounter issues with the current implementation
|
||||
2. **SOA Record Prefix**: The server hardcodes 'ns1.' prefix for SOA mname field which may not match actual nameserver names
|
||||
3. **Handler Deduplication**: If the same handler is registered multiple times, it will contribute duplicate records
|
||||
1. **DNSSEC RRset Signing**: Fixed to properly sign entire RRsets together instead of individual records
|
||||
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}')
|
||||
}
|
||||
```
|
||||
|
||||
|
@@ -1,97 +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: Planning
|
||||
**Priority: HIGH** - This issue blocks proper DNS server operation and domain registration
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
### Additional Improvements to Consider
|
||||
1. Fix DNSSEC RRSIG generation for multiple records
|
||||
2. Fix SOA record timeout issues
|
||||
3. Make DNSSEC zone prefix configurable (remove hardcoded 'ns1.')
|
||||
4. Improve error handling for edge cases
|
||||
5. Consider handler interface enhancement to return arrays
|
123
test/example.primaryns.ts
Normal file
123
test/example.primaryns.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
// Example: Using custom primary nameserver
|
||||
async function exampleCustomNameserver() {
|
||||
const dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: 'your-https-key',
|
||||
httpsCert: 'your-https-cert',
|
||||
httpsPort: 8443,
|
||||
udpPort: 8053,
|
||||
dnssecZone: 'example.com',
|
||||
// Custom primary nameserver for SOA records
|
||||
primaryNameserver: 'ns-primary.example.com',
|
||||
});
|
||||
|
||||
// Register some handlers
|
||||
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'NS',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: 'ns-primary.example.com',
|
||||
};
|
||||
});
|
||||
|
||||
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'NS',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: 'ns-secondary.example.com',
|
||||
};
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
console.log('DNS server started with custom primary nameserver');
|
||||
|
||||
// SOA records will now use 'ns-primary.example.com' instead of 'ns1.example.com'
|
||||
}
|
||||
|
||||
// Example: DNSSEC with multiple records (proper RRset signing)
|
||||
async function exampleDnssecMultipleRecords() {
|
||||
const dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: 'your-https-key',
|
||||
httpsCert: 'your-https-cert',
|
||||
httpsPort: 8443,
|
||||
udpPort: 8053,
|
||||
dnssecZone: 'secure.example.com',
|
||||
});
|
||||
|
||||
// Register multiple A records for round-robin
|
||||
const ips = ['192.168.1.10', '192.168.1.11', '192.168.1.12'];
|
||||
for (const ip of ips) {
|
||||
dnsServer.registerHandler('www.secure.example.com', ['A'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
ttl: 300,
|
||||
data: ip,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await dnsServer.start();
|
||||
console.log('DNS server started with DNSSEC and multiple A records');
|
||||
|
||||
// When queried with DNSSEC enabled, all 3 A records will be signed together
|
||||
// as a single RRset with one RRSIG record (not 3 separate RRSIGs)
|
||||
}
|
||||
|
||||
// Example: Multiple TXT records for various purposes
|
||||
async function exampleMultipleTxtRecords() {
|
||||
const dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: 'your-https-key',
|
||||
httpsCert: 'your-https-cert',
|
||||
httpsPort: 8443,
|
||||
udpPort: 8053,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// SPF record
|
||||
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'TXT',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: ['v=spf1 include:_spf.google.com ~all'],
|
||||
};
|
||||
});
|
||||
|
||||
// DKIM record
|
||||
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'TXT',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: ['v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4...'],
|
||||
};
|
||||
});
|
||||
|
||||
// Domain verification
|
||||
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'TXT',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: ['google-site-verification=1234567890abcdef'],
|
||||
};
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
console.log('DNS server started with multiple TXT records');
|
||||
|
||||
// All TXT records will be returned when queried
|
||||
}
|
||||
|
||||
// Export examples for reference
|
||||
export { exampleCustomNameserver, exampleDnssecMultipleRecords, exampleMultipleTxtRecords };
|
373
test/test.dnssec.rrset.ts
Normal file
373
test/test.dnssec.rrset.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
import * as plugins from '../ts_server/plugins.js';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
import * as dgram from 'dgram';
|
||||
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
let dnsServer: smartdns.DnsServer;
|
||||
|
||||
// Port management for tests
|
||||
let nextHttpsPort = 8500;
|
||||
let nextUdpPort = 8501;
|
||||
|
||||
function getUniqueHttpsPort() {
|
||||
return nextHttpsPort++;
|
||||
}
|
||||
|
||||
function getUniqueUdpPort() {
|
||||
return nextUdpPort++;
|
||||
}
|
||||
|
||||
// Cleanup function for servers
|
||||
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (e) {
|
||||
console.log('Handled error when stopping server:', e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('DNSSEC should sign entire RRset together, not individual records', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// Register multiple NS record handlers
|
||||
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'NS',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: 'ns1.example.com',
|
||||
};
|
||||
});
|
||||
|
||||
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'NS',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: 'ns2.example.com',
|
||||
};
|
||||
});
|
||||
|
||||
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'NS',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: 'ns3.example.com',
|
||||
};
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
// Create query with DNSSEC requested
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'example.com',
|
||||
type: 'NS',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
additionals: [
|
||||
{
|
||||
name: '.',
|
||||
type: 'OPT',
|
||||
ttl: 0,
|
||||
flags: 0x8000, // DO bit set for DNSSEC
|
||||
data: Buffer.alloc(0),
|
||||
} as any,
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
// Count NS and RRSIG records
|
||||
const nsAnswers = dnsResponse.answers.filter(a => a.type === 'NS');
|
||||
const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG');
|
||||
|
||||
console.log('NS records returned:', nsAnswers.length);
|
||||
console.log('RRSIG records returned:', rrsigAnswers.length);
|
||||
|
||||
// Should have 3 NS records and only 1 RRSIG for the entire RRset
|
||||
expect(nsAnswers.length).toEqual(3);
|
||||
expect(rrsigAnswers.length).toEqual(1);
|
||||
|
||||
// Verify RRSIG covers NS type
|
||||
const rrsigData = (rrsigAnswers[0] as any).data;
|
||||
expect(rrsigData.typeCovered).toEqual('NS');
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('SOA records should be properly serialized and returned', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
// Query for a non-existent subdomain to trigger SOA response
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 2,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
// Should have SOA record in response
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('SOA record:', soaData);
|
||||
|
||||
expect(soaData.mname).toEqual('ns1.example.com');
|
||||
expect(soaData.rname).toEqual('hostmaster.example.com');
|
||||
expect(typeof soaData.serial).toEqual('number');
|
||||
expect(soaData.refresh).toEqual(3600);
|
||||
expect(soaData.retry).toEqual(600);
|
||||
expect(soaData.expire).toEqual(604800);
|
||||
expect(soaData.minimum).toEqual(86400);
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Primary nameserver should be configurable', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
primaryNameserver: 'custom-ns.example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
// Query for SOA record
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 3,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'example.com',
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
// Should have SOA record with custom nameserver
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('SOA mname:', soaData.mname);
|
||||
|
||||
// Should use the custom primary nameserver
|
||||
expect(soaData.mname).toEqual('custom-ns.example.com');
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Multiple A records should have single RRSIG when DNSSEC is enabled', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// Register multiple A records for round-robin
|
||||
const ips = ['10.0.0.1', '10.0.0.2', '10.0.0.3'];
|
||||
for (const ip of ips) {
|
||||
dnsServer.registerHandler('www.example.com', ['A'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
ttl: 300,
|
||||
data: ip,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 4,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'www.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
additionals: [
|
||||
{
|
||||
name: '.',
|
||||
type: 'OPT',
|
||||
ttl: 0,
|
||||
flags: 0x8000, // DO bit set for DNSSEC
|
||||
data: Buffer.alloc(0),
|
||||
} as any,
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
const aAnswers = dnsResponse.answers.filter(a => a.type === 'A');
|
||||
const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG');
|
||||
|
||||
console.log('A records:', aAnswers.length);
|
||||
console.log('RRSIG records:', rrsigAnswers.length);
|
||||
|
||||
// Should have 3 A records and only 1 RRSIG
|
||||
expect(aAnswers.length).toEqual(3);
|
||||
expect(rrsigAnswers.length).toEqual(1);
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
export default tap.start();
|
228
test/test.fixes.simple.ts
Normal file
228
test/test.fixes.simple.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import * as plugins from '../ts_server/plugins.js';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
import * as dgram from 'dgram';
|
||||
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
let dnsServer: smartdns.DnsServer;
|
||||
|
||||
// Port management for tests
|
||||
let nextHttpsPort = 8600;
|
||||
let nextUdpPort = 8601;
|
||||
|
||||
function getUniqueHttpsPort() {
|
||||
return nextHttpsPort++;
|
||||
}
|
||||
|
||||
function getUniqueUdpPort() {
|
||||
return nextUdpPort++;
|
||||
}
|
||||
|
||||
// Cleanup function for servers
|
||||
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (e) {
|
||||
console.log('Handled error when stopping server:', e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('SOA records should be returned for non-existent domains', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
console.log('✅ SOA response received');
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('✅ SOA mname:', soaData.mname);
|
||||
console.log('✅ SOA rname:', soaData.rname);
|
||||
|
||||
expect(soaData.mname).toEqual('ns1.example.com');
|
||||
expect(soaData.rname).toEqual('hostmaster.example.com');
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Primary nameserver should be configurable', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
primaryNameserver: 'custom-ns.example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 2,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('✅ Custom primary nameserver:', soaData.mname);
|
||||
|
||||
expect(soaData.mname).toEqual('custom-ns.example.com');
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Default primary nameserver with FQDN', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
primaryNameserver: 'ns.example.com.', // FQDN with trailing dot
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 3,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('✅ FQDN primary nameserver:', soaData.mname);
|
||||
|
||||
expect(soaData.mname).toEqual('ns.example.com');
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
export default tap.start();
|
@@ -396,9 +396,9 @@ tap.test('should handle DNSSEC correctly with multiple records', async () => {
|
||||
console.log('NS records:', nsAnswers.length);
|
||||
console.log('RRSIG records:', rrsigAnswers.length);
|
||||
|
||||
// With DNSSEC, each NS record should have an associated RRSIG
|
||||
// With DNSSEC RRset signing, all NS records share ONE RRSIG (entire RRset signed together)
|
||||
expect(nsAnswers.length).toEqual(2);
|
||||
expect(rrsigAnswers.length).toEqual(2);
|
||||
expect(rrsigAnswers.length).toEqual(1);
|
||||
|
||||
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');
|
||||
|
269
test/test.soa.debug.ts
Normal file
269
test/test.soa.debug.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import * as plugins from '../ts_server/plugins.js';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
import * as dgram from 'dgram';
|
||||
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
let dnsServer: smartdns.DnsServer;
|
||||
|
||||
// Port management for tests
|
||||
let nextHttpsPort = 8700;
|
||||
let nextUdpPort = 8701;
|
||||
|
||||
function getUniqueHttpsPort() {
|
||||
return nextHttpsPort++;
|
||||
}
|
||||
|
||||
function getUniqueUdpPort() {
|
||||
return nextUdpPort++;
|
||||
}
|
||||
|
||||
// Cleanup function for servers
|
||||
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (e) {
|
||||
console.log('Handled error when stopping server:', e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('Direct SOA query should work without timeout', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// Register a SOA handler directly
|
||||
dnsServer.registerHandler('example.com', ['SOA'], (question) => {
|
||||
console.log('Direct SOA handler called for:', question.name);
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: {
|
||||
mname: 'ns1.example.com',
|
||||
rname: 'hostmaster.example.com',
|
||||
serial: 2024010101,
|
||||
refresh: 3600,
|
||||
retry: 600,
|
||||
expire: 604800,
|
||||
minimum: 86400,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'example.com',
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
console.log('Sending SOA query for example.com');
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out after 5 seconds'));
|
||||
}, 5000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
} catch (e) {
|
||||
reject(new Error(`Failed to decode response: ${e.message}`));
|
||||
}
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const dnsResponse = await responsePromise;
|
||||
console.log('SOA response received:', dnsResponse.answers.length, 'answers');
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('SOA data:', soaData);
|
||||
|
||||
expect(soaData.mname).toEqual('ns1.example.com');
|
||||
expect(soaData.serial).toEqual(2024010101);
|
||||
} catch (error) {
|
||||
console.error('SOA query failed:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('SOA query with DNSSEC should work', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 2,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
additionals: [
|
||||
{
|
||||
name: '.',
|
||||
type: 'OPT',
|
||||
ttl: 0,
|
||||
flags: 0x8000, // DO bit set for DNSSEC
|
||||
data: Buffer.alloc(0),
|
||||
} as any,
|
||||
],
|
||||
});
|
||||
|
||||
console.log('Sending query for nonexistent domain with DNSSEC');
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out after 5 seconds'));
|
||||
}, 5000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
} catch (e) {
|
||||
reject(new Error(`Failed to decode response: ${e.message}`));
|
||||
}
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const dnsResponse = await responsePromise;
|
||||
console.log('Response received with', dnsResponse.answers.length, 'answers');
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
console.log('SOA records found:', soaAnswers.length);
|
||||
|
||||
if (soaAnswers.length > 0) {
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('SOA data:', soaData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SOA query with DNSSEC failed:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Test raw SOA serialization', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: getUniqueUdpPort(),
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// Test the serializeRData method directly
|
||||
const soaData = {
|
||||
mname: 'ns1.example.com',
|
||||
rname: 'hostmaster.example.com',
|
||||
serial: 2024010101,
|
||||
refresh: 3600,
|
||||
retry: 600,
|
||||
expire: 604800,
|
||||
minimum: 86400,
|
||||
};
|
||||
|
||||
try {
|
||||
// @ts-ignore - accessing private method for testing
|
||||
const serialized = dnsServer.serializeRData('SOA', soaData);
|
||||
console.log('SOA serialized successfully, buffer length:', serialized.length);
|
||||
expect(serialized.length).toBeGreaterThan(0);
|
||||
|
||||
// The buffer should contain the serialized domain names + 5 * 4 bytes for the numbers
|
||||
// Domain names have variable length, but should be at least 20 bytes total
|
||||
expect(serialized.length).toBeGreaterThan(20);
|
||||
} catch (error) {
|
||||
console.error('SOA serialization failed:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
271
test/test.soa.final.ts
Normal file
271
test/test.soa.final.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import * as plugins from '../ts_server/plugins.js';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
import * as dgram from 'dgram';
|
||||
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
let dnsServer: smartdns.DnsServer;
|
||||
|
||||
// Port management for tests
|
||||
let nextHttpsPort = 8900;
|
||||
let nextUdpPort = 8901;
|
||||
|
||||
function getUniqueHttpsPort() {
|
||||
return nextHttpsPort++;
|
||||
}
|
||||
|
||||
function getUniqueUdpPort() {
|
||||
return nextUdpPort++;
|
||||
}
|
||||
|
||||
// Cleanup function for servers
|
||||
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (e) {
|
||||
console.log('Handled error when stopping server:', e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('SOA records work for all scenarios', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
primaryNameserver: 'ns.example.com',
|
||||
});
|
||||
|
||||
// Register SOA handler for the zone
|
||||
dnsServer.registerHandler('example.com', ['SOA'], (question) => {
|
||||
console.log('SOA handler called for:', question.name);
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: {
|
||||
mname: 'ns.example.com',
|
||||
rname: 'admin.example.com',
|
||||
serial: 2024010101,
|
||||
refresh: 3600,
|
||||
retry: 600,
|
||||
expire: 604800,
|
||||
minimum: 86400,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Register some other records
|
||||
dnsServer.registerHandler('example.com', ['A'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
ttl: 300,
|
||||
data: '192.168.1.1',
|
||||
};
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
// Test 1: Direct SOA query
|
||||
console.log('\n--- Test 1: Direct SOA query ---');
|
||||
const soaQuery = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'example.com',
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let response = await new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out'));
|
||||
}, 2000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.removeAllListeners();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(soaQuery, udpPort, 'localhost');
|
||||
});
|
||||
|
||||
console.log('Direct SOA query response:', response.answers.length, 'answers');
|
||||
expect(response.answers.length).toEqual(1);
|
||||
expect(response.answers[0].type).toEqual('SOA');
|
||||
|
||||
// Test 2: Non-existent domain query (should get SOA in authority)
|
||||
console.log('\n--- Test 2: Non-existent domain query ---');
|
||||
const nxQuery = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 2,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
response = await new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out'));
|
||||
}, 2000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.removeAllListeners();
|
||||
});
|
||||
|
||||
client.send(nxQuery, udpPort, 'localhost');
|
||||
});
|
||||
|
||||
console.log('Non-existent query response:', response.answers.length, 'answers');
|
||||
const soaAnswers = response.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
// Test 3: SOA with DNSSEC
|
||||
console.log('\n--- Test 3: SOA query with DNSSEC ---');
|
||||
const dnssecQuery = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 3,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'example.com',
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
additionals: [
|
||||
{
|
||||
name: '.',
|
||||
type: 'OPT',
|
||||
ttl: 0,
|
||||
flags: 0x8000, // DO bit
|
||||
data: Buffer.alloc(0),
|
||||
} as any,
|
||||
],
|
||||
});
|
||||
|
||||
response = await new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out'));
|
||||
}, 2000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.removeAllListeners();
|
||||
});
|
||||
|
||||
client.send(dnssecQuery, udpPort, 'localhost');
|
||||
});
|
||||
|
||||
console.log('DNSSEC SOA query response:', response.answers.length, 'answers');
|
||||
console.log('Answer types:', response.answers.map(a => a.type));
|
||||
expect(response.answers.length).toEqual(2); // SOA + RRSIG
|
||||
expect(response.answers.some(a => a.type === 'SOA')).toEqual(true);
|
||||
expect(response.answers.some(a => a.type === 'RRSIG')).toEqual(true);
|
||||
|
||||
client.close();
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Configurable primary nameserver works correctly', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'test.com',
|
||||
primaryNameserver: 'master.test.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.test.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out'));
|
||||
}, 2000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost');
|
||||
});
|
||||
|
||||
const soaAnswers = response.answers.filter(a => a.type === 'SOA');
|
||||
console.log('✅ Configured primary nameserver:', (soaAnswers[0] as any).data.mname);
|
||||
expect((soaAnswers[0] as any).data.mname).toEqual('master.test.com');
|
||||
|
||||
client.close();
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
export default tap.start();
|
201
test/test.soa.simple.ts
Normal file
201
test/test.soa.simple.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import * as plugins from '../ts_server/plugins.js';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
import * as dgram from 'dgram';
|
||||
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
let dnsServer: smartdns.DnsServer;
|
||||
|
||||
// Port management for tests
|
||||
let nextHttpsPort = 8800;
|
||||
let nextUdpPort = 8801;
|
||||
|
||||
function getUniqueHttpsPort() {
|
||||
return nextHttpsPort++;
|
||||
}
|
||||
|
||||
function getUniqueUdpPort() {
|
||||
return nextUdpPort++;
|
||||
}
|
||||
|
||||
// Cleanup function for servers
|
||||
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (e) {
|
||||
console.log('Handled error when stopping server:', e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('Simple SOA query without DNSSEC', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
// Query for a non-existent domain WITHOUT DNSSEC
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out'));
|
||||
}, 2000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
console.log('✅ SOA response without DNSSEC received');
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('✅ SOA data:', soaData.mname);
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Direct SOA query without DNSSEC', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// Register direct SOA handler
|
||||
dnsServer.registerHandler('example.com', ['SOA'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: {
|
||||
mname: 'ns1.example.com',
|
||||
rname: 'hostmaster.example.com',
|
||||
serial: 2024010101,
|
||||
refresh: 3600,
|
||||
retry: 600,
|
||||
expire: 604800,
|
||||
minimum: 86400,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 2,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'example.com',
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out'));
|
||||
}, 2000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
console.log('✅ Direct SOA query succeeded');
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
export default tap.start();
|
224
test/test.soa.timeout.ts
Normal file
224
test/test.soa.timeout.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import * as plugins from '../ts_server/plugins.js';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
import * as dgram from 'dgram';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
let dnsServer: smartdns.DnsServer;
|
||||
|
||||
// Port management for tests
|
||||
const testPort = 8753;
|
||||
|
||||
// Cleanup function for servers
|
||||
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (e) {
|
||||
console.log('Handled error when stopping server:', e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('Test SOA timeout with real dig command', async (tools) => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: 8752,
|
||||
udpPort: testPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
console.log(`DNS server started on port ${testPort}`);
|
||||
|
||||
// Test with dig command
|
||||
try {
|
||||
console.log('Testing SOA query with dig...');
|
||||
const result = execSync(`dig @localhost -p ${testPort} example.com SOA +timeout=3`, { encoding: 'utf8' });
|
||||
console.log('Dig SOA query result:', result);
|
||||
|
||||
// Check if we got an answer section
|
||||
expect(result).toInclude('ANSWER SECTION');
|
||||
expect(result).toInclude('SOA');
|
||||
} catch (error) {
|
||||
console.error('Dig command failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Test nonexistent domain SOA
|
||||
try {
|
||||
console.log('Testing nonexistent domain SOA query with dig...');
|
||||
const result = execSync(`dig @localhost -p ${testPort} nonexistent.example.com A +timeout=3`, { encoding: 'utf8' });
|
||||
console.log('Dig nonexistent query result:', result);
|
||||
|
||||
// Should get AUTHORITY section with SOA
|
||||
expect(result).toInclude('AUTHORITY SECTION');
|
||||
} catch (error) {
|
||||
console.error('Dig nonexistent query failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Test SOA with DNSSEC timing', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = 8754;
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: 8755,
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
// Test with DNSSEC enabled
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
additionals: [
|
||||
{
|
||||
name: '.',
|
||||
type: 'OPT',
|
||||
ttl: 0,
|
||||
flags: 0x8000, // DO bit set for DNSSEC
|
||||
data: Buffer.alloc(0),
|
||||
} as any,
|
||||
],
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
console.log('Sending DNSSEC query for nonexistent domain...');
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
const elapsed = Date.now() - startTime;
|
||||
reject(new Error(`Query timed out after ${elapsed}ms`));
|
||||
}, 3000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
const elapsed = Date.now() - startTime;
|
||||
console.log(`Response received in ${elapsed}ms`);
|
||||
|
||||
try {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
} catch (e) {
|
||||
reject(new Error(`Failed to decode response: ${e.message}`));
|
||||
}
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
const elapsed = Date.now() - startTime;
|
||||
console.error(`Error after ${elapsed}ms:`, err);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const dnsResponse = await responsePromise;
|
||||
console.log('Response details:');
|
||||
console.log('- Answers:', dnsResponse.answers.length);
|
||||
console.log('- Answer types:', dnsResponse.answers.map(a => a.type));
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG');
|
||||
|
||||
console.log('- SOA records:', soaAnswers.length);
|
||||
console.log('- RRSIG records:', rrsigAnswers.length);
|
||||
|
||||
// With the fix, SOA should have its RRSIG
|
||||
if (soaAnswers.length > 0) {
|
||||
expect(rrsigAnswers.length).toBeGreaterThan(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('DNSSEC SOA query failed:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Check DNSSEC signing performance for SOA', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: 8756,
|
||||
udpPort: 8757,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// Time SOA serialization
|
||||
const soaData = {
|
||||
mname: 'ns1.example.com',
|
||||
rname: 'hostmaster.example.com',
|
||||
serial: 2024010101,
|
||||
refresh: 3600,
|
||||
retry: 600,
|
||||
expire: 604800,
|
||||
minimum: 86400,
|
||||
};
|
||||
|
||||
console.log('Testing SOA serialization performance...');
|
||||
const serializeStart = Date.now();
|
||||
|
||||
try {
|
||||
// @ts-ignore - accessing private method for testing
|
||||
const serialized = dnsServer.serializeRData('SOA', soaData);
|
||||
const serializeTime = Date.now() - serializeStart;
|
||||
console.log(`SOA serialization took ${serializeTime}ms`);
|
||||
|
||||
// Test DNSSEC signing
|
||||
const signStart = Date.now();
|
||||
// @ts-ignore - accessing private property
|
||||
const signature = dnsServer.dnsSec.signData(serialized);
|
||||
const signTime = Date.now() - signStart;
|
||||
console.log(`DNSSEC signing took ${signTime}ms`);
|
||||
|
||||
expect(serializeTime).toBeLessThan(100); // Should be fast
|
||||
expect(signTime).toBeLessThan(500); // Signing can take longer but shouldn't timeout
|
||||
} catch (error) {
|
||||
console.error('Performance test failed:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartdns',
|
||||
version: '7.4.2',
|
||||
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.'
|
||||
}
|
||||
|
@@ -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,27 +147,111 @@ 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;
|
||||
};
|
||||
|
||||
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) {
|
||||
if (dnsEntry.data.startsWith('"') && dnsEntry.data.endsWith('"')) {
|
||||
dnsEntry.data = dnsEntry.data.replace(/^"(.*)"$/, '$1');
|
||||
|
@@ -13,6 +13,10 @@ 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;
|
||||
// Local handling for RFC 6761 localhost (default: true)
|
||||
enableLocalhostHandling?: boolean;
|
||||
}
|
||||
|
||||
export interface DnsAnswer {
|
||||
@@ -560,10 +564,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;
|
||||
let shouldSignRrset = true; // skip DNSSEC signing for synthetic/local answers
|
||||
const recordsForQuestion: DnsAnswer[] = [];
|
||||
|
||||
// Handle DNSKEY queries if DNSSEC is requested
|
||||
if (dnssecRequested && question.type === 'DNSKEY' && question.name === this.options.dnssecZone) {
|
||||
@@ -574,40 +583,92 @@ 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;
|
||||
// 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
|
||||
if (!answered) {
|
||||
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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// Group records by type for DNSSEC signing
|
||||
if (dnssecRequested && shouldSignRrset) {
|
||||
const rrsetKey = `${question.name}:${question.type}`;
|
||||
rrsetMap.set(rrsetKey, recordsForQuestion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,7 +681,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,
|
||||
@@ -630,6 +691,22 @@ export class DnsServer {
|
||||
},
|
||||
};
|
||||
response.answers.push(soaAnswer as plugins.dnsPacket.Answer);
|
||||
|
||||
// Add SOA record to DNSSEC signing map if DNSSEC is requested
|
||||
if (dnssecRequested) {
|
||||
const soaKey = `${question.name}:SOA`;
|
||||
rrsetMap.set(soaKey, [soaAnswer]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,6 +745,17 @@ export class DnsServer {
|
||||
// Sign the RRset
|
||||
const signature = this.dnsSec.signData(rrsetBuffer);
|
||||
|
||||
// Ensure all fields are defined
|
||||
if (!signerName || !signature) {
|
||||
console.error('RRSIG generation error - missing fields:', {
|
||||
signerName,
|
||||
signature: signature ? 'present' : 'missing',
|
||||
algorithm,
|
||||
keyTag,
|
||||
type
|
||||
});
|
||||
}
|
||||
|
||||
// Construct the RRSIG record
|
||||
const rrsig: DnsAnswer = {
|
||||
name,
|
||||
@@ -675,15 +763,15 @@ export class DnsServer {
|
||||
class: 'IN',
|
||||
ttl,
|
||||
data: {
|
||||
typeCovered: type, // Changed to type string
|
||||
typeCovered: type, // dns-packet expects the string type
|
||||
algorithm,
|
||||
labels: name.split('.').length - 1,
|
||||
labels: name.split('.').filter(l => l.length > 0).length, // Fix label count
|
||||
originalTTL: ttl,
|
||||
expiration,
|
||||
inception,
|
||||
keyTag,
|
||||
signerName,
|
||||
signature: signature,
|
||||
signersName: signerName || this.options.dnssecZone, // Note: signersName with 's'
|
||||
signature: signature || Buffer.alloc(0), // Fallback to empty buffer
|
||||
},
|
||||
};
|
||||
|
||||
@@ -760,9 +848,27 @@ 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]);
|
||||
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.`);
|
||||
@@ -967,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);
|
||||
@@ -975,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