Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
1b9eefd70f | |||
f29962a6dc | |||
afd1c18496 | |||
0ea622aa8d | |||
56a33dd7ae | |||
9e5fae055f | |||
afdd6a6074 | |||
3d06131e04 | |||
1811ebd4d4 | |||
e7ace9b596 | |||
f6175d1f2b | |||
d67fbc87e2 | |||
b87cbbee5c | |||
4e37bc9bc0 | |||
2b97dffb47 | |||
e7cb0921fc | |||
0f8953fc1d |
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"
|
62
changelog.md
62
changelog.md
@@ -1,5 +1,67 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-09-12 - 7.6.0 - feat(dnsserver)
|
||||||
|
Return multiple matching records, improve DNSSEC RRset signing, add client resolution strategy and localhost handling, update tests
|
||||||
|
|
||||||
|
- Server: process all matching handlers for a question so multiple records (NS, A, TXT, etc.) are returned instead of stopping after the first match
|
||||||
|
- DNSSEC: sign entire RRsets together (single RRSIG per RRset) and ensure DNSKEY/DS generation and key-tag computation are handled correctly
|
||||||
|
- Server: built-in localhost handling (RFC 6761) with an enableLocalhostHandling option and synthetic answers for localhost/127.0.0.1 reverse lookups
|
||||||
|
- Server: improved SOA generation (primary nameserver handling), name serialization (trim trailing dot), and safer start/stop behavior
|
||||||
|
- Client: added resolution strategy options (doh | system | prefer-system), allowDohFallback and per-query timeout support; improved DoH and system lookup handling (proper TXT quoting and name trimming)
|
||||||
|
- Tests: updated expectations and test descriptions to reflect correct multi-record behavior and other fixes
|
||||||
|
|
||||||
|
## 2025-09-12 - 7.5.1 - fix(dependencies)
|
||||||
|
Bump dependency versions and add pnpm workspace onlyBuiltDependencies
|
||||||
|
|
||||||
|
- Bumped @push.rocks/smartenv from ^5.0.5 to ^5.0.13
|
||||||
|
- Bumped @git.zone/tsbuild from ^2.6.4 to ^2.6.8
|
||||||
|
- Bumped @git.zone/tstest from ^2.3.1 to ^2.3.7
|
||||||
|
- Added pnpm-workspace.yaml with onlyBuiltDependencies: [esbuild, mongodb-memory-server, puppeteer]
|
||||||
|
|
||||||
|
## 2025-06-01 - 7.5.0 - feat(dnssec)
|
||||||
|
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.
|
||||||
|
|
||||||
|
- Removed the break statement in processDnsRequest to allow all matching handlers to contribute responses.
|
||||||
|
- Updated NS record serialization to properly handle domain names in DNSSEC context.
|
||||||
|
- Enhanced tests for round-robin A records and multiple TXT records scenarios.
|
||||||
|
|
||||||
|
## 2025-05-28 - 7.4.1 - fix(test/server)
|
||||||
|
Fix force cleanup in DNS server tests by casting server properties before closing sockets
|
||||||
|
|
||||||
|
- Cast server to any to safely invoke close() on httpsServer and udpServer in test cleanup
|
||||||
|
- Ensures proper emergency cleanup of server sockets without direct access to private properties
|
||||||
|
|
||||||
## 2025-05-28 - 7.4.0 - feat(manual socket handling)
|
## 2025-05-28 - 7.4.0 - feat(manual socket handling)
|
||||||
Add comprehensive manual socket handling documentation for advanced DNS server use cases
|
Add comprehensive manual socket handling documentation for advanced DNS server use cases
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartdns",
|
"name": "@push.rocks/smartdns",
|
||||||
"version": "7.4.0",
|
"version": "7.6.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.",
|
"description": "A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"homepage": "https://code.foss.global/push.rocks/smartdns",
|
"homepage": "https://code.foss.global/push.rocks/smartdns",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/smartdelay": "^3.0.1",
|
"@push.rocks/smartdelay": "^3.0.1",
|
||||||
"@push.rocks/smartenv": "^5.0.5",
|
"@push.rocks/smartenv": "^5.0.13",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrequest": "^2.1.0",
|
"@push.rocks/smartrequest": "^2.1.0",
|
||||||
"@tsclass/tsclass": "^9.2.0",
|
"@tsclass/tsclass": "^9.2.0",
|
||||||
@@ -56,9 +56,9 @@
|
|||||||
"minimatch": "^10.0.1"
|
"minimatch": "^10.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.6.4",
|
"@git.zone/tsbuild": "^2.6.8",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
"@git.zone/tstest": "^2.3.1",
|
"@git.zone/tstest": "^2.3.7",
|
||||||
"@types/node": "^22.15.21"
|
"@types/node": "^22.15.21"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
4613
pnpm-lock.yaml
generated
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
|
@@ -54,9 +54,17 @@ The smartdns library is structured into three main modules:
|
|||||||
### Handler System:
|
### Handler System:
|
||||||
- Pattern-based domain matching using minimatch
|
- Pattern-based domain matching using minimatch
|
||||||
- Support for all common record types
|
- Support for all common record types
|
||||||
|
- **Multiple Handler Support**: As of v7.4.2+, multiple handlers can contribute records of the same type
|
||||||
- Handler chaining for complex scenarios
|
- Handler chaining for complex scenarios
|
||||||
- Automatic SOA response for unhandled queries
|
- Automatic SOA response for unhandled queries
|
||||||
|
|
||||||
|
### Multiple Records Support (v7.4.2+):
|
||||||
|
- Server now processes ALL matching handlers for a query (previously stopped after first match)
|
||||||
|
- Enables proper multi-NS record support for domain registration
|
||||||
|
- Supports round-robin DNS with multiple A/AAAA records
|
||||||
|
- Allows multiple TXT records (SPF, DKIM, domain verification)
|
||||||
|
- Each handler contributes its record to the response
|
||||||
|
|
||||||
## Key Dependencies
|
## Key Dependencies
|
||||||
|
|
||||||
- `dns-packet`: DNS packet encoding/decoding (wire format)
|
- `dns-packet`: DNS packet encoding/decoding (wire format)
|
||||||
@@ -94,4 +102,21 @@ The test suite demonstrates:
|
|||||||
- DNSSEC provides authentication but not encryption
|
- DNSSEC provides authentication but not encryption
|
||||||
- DoH (DNS-over-HTTPS) provides both privacy and integrity
|
- DoH (DNS-over-HTTPS) provides both privacy and integrity
|
||||||
- Let's Encrypt integration requires proper domain authorization
|
- Let's Encrypt integration requires proper domain authorization
|
||||||
- Handler patterns should be carefully designed to avoid open resolvers
|
- Handler patterns should be carefully designed to avoid open resolvers
|
||||||
|
|
||||||
|
## Recent Improvements (v7.4.3)
|
||||||
|
|
||||||
|
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',
|
httpsCert: 'path/to/cert.pem',
|
||||||
dnssecZone: 'example.com',
|
dnssecZone: 'example.com',
|
||||||
udpBindInterface: '127.0.0.1', // Bind UDP to localhost only
|
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
|
// Register a handler for all subdomains of example.com
|
||||||
@@ -224,6 +225,35 @@ await dnsServer.start();
|
|||||||
console.log('DNS Server started!');
|
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
|
### DNSSEC Support
|
||||||
|
|
||||||
The DNS server includes comprehensive DNSSEC support with automatic key generation and record signing:
|
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
|
```typescript
|
||||||
export interface IDnsServerOptions {
|
export interface IDnsServerOptions {
|
||||||
// ... standard options ...
|
httpsKey: string; // Path or content of HTTPS private key
|
||||||
manualUdpMode?: boolean; // Handle UDP sockets manually
|
httpsCert: string; // Path or content of HTTPS certificate
|
||||||
manualHttpsMode?: boolean; // Handle HTTPS sockets manually
|
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}')
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
103
readme.plan.md
103
readme.plan.md
@@ -1,103 +0,0 @@
|
|||||||
# DNS Server Interface Binding Implementation Plan
|
|
||||||
|
|
||||||
Command to reread CLAUDE.md: `cat /home/philkunz/.claude/CLAUDE.md`
|
|
||||||
|
|
||||||
## Overview ✅ COMPLETED
|
|
||||||
Enable specific interface binding for the DNSServer class to allow binding to specific network interfaces instead of all interfaces (0.0.0.0).
|
|
||||||
|
|
||||||
## Implementation Status: COMPLETED ✅
|
|
||||||
|
|
||||||
### What was implemented:
|
|
||||||
|
|
||||||
✅ **1. Updated IDnsServerOptions Interface**
|
|
||||||
- Added optional `udpBindInterface?: string` property (defaults to '0.0.0.0')
|
|
||||||
- Added optional `httpsBindInterface?: string` property (defaults to '0.0.0.0')
|
|
||||||
- Located in `ts_server/classes.dnsserver.ts:5-11`
|
|
||||||
|
|
||||||
✅ **2. Modified DnsServer.start() Method**
|
|
||||||
- Updated UDP server binding to use `this.options.udpBindInterface || '0.0.0.0'`
|
|
||||||
- Updated HTTPS server listening to use `this.options.httpsBindInterface || '0.0.0.0'`
|
|
||||||
- Added IP address validation before binding
|
|
||||||
- Updated console logging to show specific interface being bound
|
|
||||||
- Located in `ts_server/classes.dnsserver.ts:699-752`
|
|
||||||
|
|
||||||
✅ **3. Added IP Address Validation**
|
|
||||||
- Created `isValidIpAddress()` method supporting IPv4 and IPv6
|
|
||||||
- Validates interface addresses before binding
|
|
||||||
- Throws meaningful error messages for invalid addresses
|
|
||||||
- Located in `ts_server/classes.dnsserver.ts:392-398`
|
|
||||||
|
|
||||||
✅ **4. Updated Documentation**
|
|
||||||
- Added dedicated "Interface Binding" section to readme.md
|
|
||||||
- Included examples for localhost-only binding (`127.0.0.1`, `::1`)
|
|
||||||
- Documented security considerations and use cases
|
|
||||||
- Added examples for specific interface binding
|
|
||||||
|
|
||||||
✅ **5. Added Comprehensive Tests**
|
|
||||||
- **localhost binding test**: Verifies binding to `127.0.0.1` instead of `0.0.0.0`
|
|
||||||
- **Invalid IP validation test**: Ensures invalid IP addresses are rejected
|
|
||||||
- **IPv6 support test**: Tests `::1` binding (with graceful fallback if IPv6 unavailable)
|
|
||||||
- **Backwards compatibility**: Existing tests continue to work with default behavior
|
|
||||||
- Located in `test/test.server.ts`
|
|
||||||
|
|
||||||
✅ **6. Updated restartHttpsServer Method**
|
|
||||||
- Modified to respect interface binding options during certificate updates
|
|
||||||
- Ensures Let's Encrypt certificate renewal maintains interface binding
|
|
||||||
|
|
||||||
## ✅ Implementation Results
|
|
||||||
|
|
||||||
### Test Results
|
|
||||||
All interface binding functionality has been successfully tested:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
✅ should bind to localhost interface only (318ms)
|
|
||||||
- UDP DNS server running on 127.0.0.1:8085
|
|
||||||
- HTTPS DNS server running on 127.0.0.1:8084
|
|
||||||
|
|
||||||
✅ should reject invalid IP addresses (151ms)
|
|
||||||
- Validates IP address format correctly
|
|
||||||
- Throws meaningful error messages
|
|
||||||
|
|
||||||
✅ should work with IPv6 localhost if available
|
|
||||||
- Gracefully handles IPv6 unavailability in containerized environments
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benefits Achieved
|
|
||||||
- ✅ Enhanced security by allowing localhost-only binding
|
|
||||||
- ✅ Support for multi-homed servers with specific interface requirements
|
|
||||||
- ✅ Better isolation in containerized environments
|
|
||||||
- ✅ Backwards compatible (defaults to current behavior)
|
|
||||||
- ✅ IP address validation with clear error messages
|
|
||||||
- ✅ IPv4 and IPv6 support
|
|
||||||
|
|
||||||
## Example Usage (Now Available)
|
|
||||||
```typescript
|
|
||||||
// Bind to localhost only
|
|
||||||
const dnsServer = new DnsServer({
|
|
||||||
httpsKey: cert.key,
|
|
||||||
httpsCert: cert.cert,
|
|
||||||
httpsPort: 443,
|
|
||||||
udpPort: 53,
|
|
||||||
dnssecZone: 'example.com',
|
|
||||||
udpBindInterface: '127.0.0.1',
|
|
||||||
httpsBindInterface: '127.0.0.1'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bind to specific interface
|
|
||||||
const dnsServer = new DnsServer({
|
|
||||||
// ... other options
|
|
||||||
udpBindInterface: '192.168.1.100',
|
|
||||||
httpsBindInterface: '192.168.1.100'
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files to Modify
|
|
||||||
1. `ts_server/classes.dnsserver.ts` - Interface and implementation
|
|
||||||
2. `readme.md` - Documentation updates
|
|
||||||
3. `test/test.server.ts` - Add interface binding tests
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
- Unit tests for interface validation
|
|
||||||
- Integration tests for binding behavior
|
|
||||||
- Error handling tests for invalid interfaces
|
|
||||||
- Backwards compatibility tests
|
|
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();
|
485
test/test.multiplerecords.fixed.ts
Normal file
485
test/test.multiplerecords.fixed.ts
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
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 = 8300;
|
||||||
|
let nextUdpPort = 8301;
|
||||||
|
|
||||||
|
function getUniqueHttpsPort() {
|
||||||
|
return nextHttpsPort++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUniqueUdpPort() {
|
||||||
|
return nextUdpPort++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup function for servers
|
||||||
|
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||||
|
if (!server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stopPromise = server.stop();
|
||||||
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
|
setTimeout(() => reject(new Error('Stop operation timed out')), 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.race([stopPromise, timeoutPromise]);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Handled error when stopping server:', e.message || e);
|
||||||
|
|
||||||
|
// Force close if normal stop fails
|
||||||
|
try {
|
||||||
|
// @ts-ignore - accessing private properties for emergency cleanup
|
||||||
|
if (server.httpsServer) {
|
||||||
|
(server as any).httpsServer.close();
|
||||||
|
(server as any).httpsServer = null;
|
||||||
|
}
|
||||||
|
// @ts-ignore - accessing private properties for emergency cleanup
|
||||||
|
if (server.udpServer) {
|
||||||
|
(server as any).udpServer.close();
|
||||||
|
(server as any).udpServer = null;
|
||||||
|
}
|
||||||
|
} catch (forceError) {
|
||||||
|
console.log('Force cleanup error:', forceError.message || forceError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tap.test('should now return multiple NS records after fix', 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 for the same domain
|
||||||
|
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||||
|
console.log('First NS handler called');
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'NS',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 3600,
|
||||||
|
data: 'ns1.example.com',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||||
|
console.log('Second NS handler called');
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'NS',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 3600,
|
||||||
|
data: 'ns2.example.com',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 'NS',
|
||||||
|
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('Fixed behavior - NS records returned:', dnsResponse.answers.length);
|
||||||
|
console.log('NS records:', dnsResponse.answers.filter(a => a.type === 'NS').map(a => a.data));
|
||||||
|
|
||||||
|
// FIXED BEHAVIOR: Should now return both NS records
|
||||||
|
const nsAnswers = dnsResponse.answers.filter(a => a.type === 'NS');
|
||||||
|
expect(nsAnswers.length).toEqual(2);
|
||||||
|
expect(nsAnswers.map(a => a.data).sort()).toEqual(['ns1.example.com', 'ns2.example.com']);
|
||||||
|
|
||||||
|
await stopServer(dnsServer);
|
||||||
|
dnsServer = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should support round-robin DNS with multiple A 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 A record handlers for round-robin DNS
|
||||||
|
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) => {
|
||||||
|
console.log(`A handler for ${ip} called`);
|
||||||
|
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: 2,
|
||||||
|
flags: dnsPacket.RECURSION_DESIRED,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
name: 'www.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('Fixed behavior - A records returned:', dnsResponse.answers.length);
|
||||||
|
console.log('A records:', dnsResponse.answers.filter(a => a.type === 'A').map(a => a.data));
|
||||||
|
|
||||||
|
// FIXED BEHAVIOR: Should return all A records for round-robin
|
||||||
|
const aAnswers = dnsResponse.answers.filter(a => a.type === 'A');
|
||||||
|
expect(aAnswers.length).toEqual(3);
|
||||||
|
expect(aAnswers.map(a => a.data).sort()).toEqual(['10.0.0.1', '10.0.0.2', '10.0.0.3']);
|
||||||
|
|
||||||
|
await stopServer(dnsServer);
|
||||||
|
dnsServer = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should return multiple TXT 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 TXT record handlers
|
||||||
|
const txtRecords = [
|
||||||
|
['v=spf1 include:_spf.example.com ~all'],
|
||||||
|
['v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNA...'],
|
||||||
|
['google-site-verification=1234567890abcdef']
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const data of txtRecords) {
|
||||||
|
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
|
||||||
|
console.log(`TXT handler for ${data[0].substring(0, 20)}... called`);
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'TXT',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 3600,
|
||||||
|
data: data,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await dnsServer.start();
|
||||||
|
|
||||||
|
const client = dgram.createSocket('udp4');
|
||||||
|
|
||||||
|
const query = dnsPacket.encode({
|
||||||
|
type: 'query',
|
||||||
|
id: 3,
|
||||||
|
flags: dnsPacket.RECURSION_DESIRED,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
name: 'example.com',
|
||||||
|
type: 'TXT',
|
||||||
|
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('Fixed behavior - TXT records returned:', dnsResponse.answers.length);
|
||||||
|
const txtAnswers = dnsResponse.answers.filter(a => a.type === 'TXT');
|
||||||
|
console.log('TXT records count:', txtAnswers.length);
|
||||||
|
|
||||||
|
// FIXED BEHAVIOR: Should return all TXT records
|
||||||
|
expect(txtAnswers.length).toEqual(3);
|
||||||
|
|
||||||
|
// Check that all expected records are present
|
||||||
|
const txtData = txtAnswers.map(a => a.data[0].toString());
|
||||||
|
expect(txtData.some(d => d.includes('spf1'))).toEqual(true);
|
||||||
|
expect(txtData.some(d => d.includes('DKIM1'))).toEqual(true);
|
||||||
|
expect(txtData.some(d => d.includes('google-site-verification'))).toEqual(true);
|
||||||
|
|
||||||
|
await stopServer(dnsServer);
|
||||||
|
dnsServer = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should handle DNSSEC correctly with multiple 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',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await dnsServer.start();
|
||||||
|
|
||||||
|
const client = dgram.createSocket('udp4');
|
||||||
|
|
||||||
|
// Create query with DNSSEC requested
|
||||||
|
const query = dnsPacket.encode({
|
||||||
|
type: 'query',
|
||||||
|
id: 4,
|
||||||
|
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;
|
||||||
|
|
||||||
|
console.log('DNSSEC response - total answers:', dnsResponse.answers.length);
|
||||||
|
|
||||||
|
const nsAnswers = dnsResponse.answers.filter(a => a.type === 'NS');
|
||||||
|
const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG');
|
||||||
|
|
||||||
|
console.log('NS records:', nsAnswers.length);
|
||||||
|
console.log('RRSIG records:', rrsigAnswers.length);
|
||||||
|
|
||||||
|
// With DNSSEC RRset signing, all NS records share ONE RRSIG (entire RRset signed together)
|
||||||
|
expect(nsAnswers.length).toEqual(2);
|
||||||
|
expect(rrsigAnswers.length).toEqual(1);
|
||||||
|
|
||||||
|
await stopServer(dnsServer);
|
||||||
|
dnsServer = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should not return duplicate records when same handler registered multiple times', 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 the same handler multiple times (edge case)
|
||||||
|
const sameHandler = (question) => {
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'A',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 300,
|
||||||
|
data: '10.0.0.1',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
dnsServer.registerHandler('test.example.com', ['A'], sameHandler);
|
||||||
|
dnsServer.registerHandler('test.example.com', ['A'], sameHandler);
|
||||||
|
dnsServer.registerHandler('test.example.com', ['A'], sameHandler);
|
||||||
|
|
||||||
|
await dnsServer.start();
|
||||||
|
|
||||||
|
const client = dgram.createSocket('udp4');
|
||||||
|
|
||||||
|
const query = dnsPacket.encode({
|
||||||
|
type: 'query',
|
||||||
|
id: 5,
|
||||||
|
flags: dnsPacket.RECURSION_DESIRED,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
name: 'test.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 aAnswers = dnsResponse.answers.filter(a => a.type === 'A');
|
||||||
|
console.log('Duplicate handler test - A records returned:', aAnswers.length);
|
||||||
|
|
||||||
|
// Even though handler is registered 3 times, we get 3 identical records
|
||||||
|
// This is expected behavior - the DNS server doesn't deduplicate
|
||||||
|
expect(aAnswers.length).toEqual(3);
|
||||||
|
expect(aAnswers.every(a => a.data === '10.0.0.1')).toEqual(true);
|
||||||
|
|
||||||
|
await stopServer(dnsServer);
|
||||||
|
dnsServer = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
279
test/test.multiplerecords.simple.ts
Normal file
279
test/test.multiplerecords.simple.ts
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
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 = 8400;
|
||||||
|
let nextUdpPort = 8401;
|
||||||
|
|
||||||
|
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('Multiple NS records should work 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: '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',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 'NS',
|
||||||
|
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('✅ NS records returned:', dnsResponse.answers.length);
|
||||||
|
console.log('✅ NS records:', dnsResponse.answers.map(a => (a as any).data));
|
||||||
|
|
||||||
|
// SUCCESS: Multiple NS records are now returned
|
||||||
|
expect(dnsResponse.answers.length).toEqual(2);
|
||||||
|
expect(dnsResponse.answers.map(a => (a as any).data).sort()).toEqual(['ns1.example.com', 'ns2.example.com']);
|
||||||
|
|
||||||
|
await stopServer(dnsServer);
|
||||||
|
dnsServer = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Multiple A records for round-robin DNS', 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
|
||||||
|
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: 2,
|
||||||
|
flags: dnsPacket.RECURSION_DESIRED,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
name: 'www.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('✅ A records returned:', dnsResponse.answers.length);
|
||||||
|
console.log('✅ A records:', dnsResponse.answers.map(a => (a as any).data));
|
||||||
|
|
||||||
|
// SUCCESS: All A records for round-robin DNS
|
||||||
|
expect(dnsResponse.answers.length).toEqual(3);
|
||||||
|
expect(dnsResponse.answers.map(a => (a as any).data).sort()).toEqual(['10.0.0.1', '10.0.0.2', '10.0.0.3']);
|
||||||
|
|
||||||
|
await stopServer(dnsServer);
|
||||||
|
dnsServer = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Multiple TXT 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 TXT records
|
||||||
|
const txtRecords = [
|
||||||
|
['v=spf1 include:_spf.example.com ~all'],
|
||||||
|
['v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNA...'],
|
||||||
|
['google-site-verification=1234567890abcdef']
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const data of txtRecords) {
|
||||||
|
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'TXT',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 3600,
|
||||||
|
data: data,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await dnsServer.start();
|
||||||
|
|
||||||
|
const client = dgram.createSocket('udp4');
|
||||||
|
|
||||||
|
const query = dnsPacket.encode({
|
||||||
|
type: 'query',
|
||||||
|
id: 3,
|
||||||
|
flags: dnsPacket.RECURSION_DESIRED,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
name: 'example.com',
|
||||||
|
type: 'TXT',
|
||||||
|
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('✅ TXT records returned:', dnsResponse.answers.length);
|
||||||
|
|
||||||
|
// SUCCESS: All TXT records are returned
|
||||||
|
expect(dnsResponse.answers.length).toEqual(3);
|
||||||
|
|
||||||
|
const txtData = dnsResponse.answers.map(a => (a as any).data[0].toString());
|
||||||
|
expect(txtData.some(d => d.includes('spf1'))).toEqual(true);
|
||||||
|
expect(txtData.some(d => d.includes('DKIM1'))).toEqual(true);
|
||||||
|
expect(txtData.some(d => d.includes('google-site-verification'))).toEqual(true);
|
||||||
|
|
||||||
|
await stopServer(dnsServer);
|
||||||
|
dnsServer = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
424
test/test.multiplerecords.ts
Normal file
424
test/test.multiplerecords.ts
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
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 = 8200;
|
||||||
|
let nextUdpPort = 8201;
|
||||||
|
|
||||||
|
function getUniqueHttpsPort() {
|
||||||
|
return nextHttpsPort++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUniqueUdpPort() {
|
||||||
|
return nextUdpPort++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup function for servers
|
||||||
|
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||||
|
if (!server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stopPromise = server.stop();
|
||||||
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
|
setTimeout(() => reject(new Error('Stop operation timed out')), 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.race([stopPromise, timeoutPromise]);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Handled error when stopping server:', e.message || e);
|
||||||
|
|
||||||
|
// Force close if normal stop fails
|
||||||
|
try {
|
||||||
|
// @ts-ignore - accessing private properties for emergency cleanup
|
||||||
|
if (server.httpsServer) {
|
||||||
|
(server as any).httpsServer.close();
|
||||||
|
(server as any).httpsServer = null;
|
||||||
|
}
|
||||||
|
// @ts-ignore - accessing private properties for emergency cleanup
|
||||||
|
if (server.udpServer) {
|
||||||
|
(server as any).udpServer.close();
|
||||||
|
(server as any).udpServer = null;
|
||||||
|
}
|
||||||
|
} catch (forceError) {
|
||||||
|
console.log('Force cleanup error:', forceError.message || forceError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tap.test('should properly return multiple NS 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 for the same domain
|
||||||
|
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||||
|
console.log('First NS handler called');
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'NS',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 3600,
|
||||||
|
data: 'ns1.example.com',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||||
|
console.log('Second NS handler called');
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'NS',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 3600,
|
||||||
|
data: 'ns2.example.com',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 'NS',
|
||||||
|
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('Current behavior - NS records returned:', dnsResponse.answers.length);
|
||||||
|
console.log('NS records:', dnsResponse.answers.map(a => (a as any).data));
|
||||||
|
|
||||||
|
// 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 properly return multiple A records for round-robin DNS', 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 record handlers for round-robin DNS
|
||||||
|
dnsServer.registerHandler('www.example.com', ['A'], (question) => {
|
||||||
|
console.log('First A handler called');
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'A',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 300,
|
||||||
|
data: '10.0.0.1',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dnsServer.registerHandler('www.example.com', ['A'], (question) => {
|
||||||
|
console.log('Second A handler called');
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'A',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 300,
|
||||||
|
data: '10.0.0.2',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dnsServer.registerHandler('www.example.com', ['A'], (question) => {
|
||||||
|
console.log('Third A handler called');
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'A',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 300,
|
||||||
|
data: '10.0.0.3',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await dnsServer.start();
|
||||||
|
|
||||||
|
const client = dgram.createSocket('udp4');
|
||||||
|
|
||||||
|
const query = dnsPacket.encode({
|
||||||
|
type: 'query',
|
||||||
|
id: 2,
|
||||||
|
flags: dnsPacket.RECURSION_DESIRED,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
name: 'www.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('Current behavior - A records returned:', dnsResponse.answers.length);
|
||||||
|
console.log('A records:', dnsResponse.answers.map(a => (a as any).data));
|
||||||
|
|
||||||
|
// 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 properly return multiple TXT 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 TXT record handlers
|
||||||
|
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
|
||||||
|
console.log('SPF handler called');
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'TXT',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 3600,
|
||||||
|
data: ['v=spf1 include:_spf.example.com ~all'],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
|
||||||
|
console.log('DKIM handler called');
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'TXT',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 3600,
|
||||||
|
data: ['v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNA...'],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
|
||||||
|
console.log('Domain verification handler called');
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'TXT',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 3600,
|
||||||
|
data: ['google-site-verification=1234567890abcdef'],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await dnsServer.start();
|
||||||
|
|
||||||
|
const client = dgram.createSocket('udp4');
|
||||||
|
|
||||||
|
const query = dnsPacket.encode({
|
||||||
|
type: 'query',
|
||||||
|
id: 3,
|
||||||
|
flags: dnsPacket.RECURSION_DESIRED,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
name: 'example.com',
|
||||||
|
type: 'TXT',
|
||||||
|
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('Current behavior - TXT records returned:', dnsResponse.answers.length);
|
||||||
|
console.log('TXT records:', dnsResponse.answers.map(a => (a as any).data));
|
||||||
|
|
||||||
|
// 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 rotate between records when using a single handler', 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',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pattern: Create an array to store NS records and rotate through them
|
||||||
|
const nsRecords = ['ns1.example.com', 'ns2.example.com'];
|
||||||
|
let nsIndex = 0;
|
||||||
|
|
||||||
|
// This pattern rotates between records on successive queries
|
||||||
|
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||||
|
const record = nsRecords[nsIndex % nsRecords.length];
|
||||||
|
nsIndex++;
|
||||||
|
return {
|
||||||
|
name: question.name,
|
||||||
|
type: 'NS',
|
||||||
|
class: 'IN',
|
||||||
|
ttl: 3600,
|
||||||
|
data: record,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await dnsServer.start();
|
||||||
|
|
||||||
|
// Make two queries to show the workaround behavior
|
||||||
|
const client1 = dgram.createSocket('udp4');
|
||||||
|
const client2 = dgram.createSocket('udp4');
|
||||||
|
|
||||||
|
const query = dnsPacket.encode({
|
||||||
|
type: 'query',
|
||||||
|
id: 4,
|
||||||
|
flags: dnsPacket.RECURSION_DESIRED,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
name: 'example.com',
|
||||||
|
type: 'NS',
|
||||||
|
class: 'IN',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const responsePromise1 = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||||
|
client1.on('message', (msg) => {
|
||||||
|
const dnsResponse = dnsPacket.decode(msg);
|
||||||
|
resolve(dnsResponse);
|
||||||
|
client1.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
client1.send(query, udpPort, 'localhost');
|
||||||
|
});
|
||||||
|
|
||||||
|
const responsePromise2 = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||||
|
client2.on('message', (msg) => {
|
||||||
|
const dnsResponse = dnsPacket.decode(msg);
|
||||||
|
resolve(dnsResponse);
|
||||||
|
client2.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
client2.send(query, udpPort, 'localhost');
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [response1, response2] = await Promise.all([responsePromise1, responsePromise2]);
|
||||||
|
|
||||||
|
console.log('First query NS:', (response1.answers[0] as any).data);
|
||||||
|
console.log('Second query NS:', (response2.answers[0] as any).data);
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
expect((response2.answers[0] as any).data).toEqual('ns2.example.com');
|
||||||
|
|
||||||
|
await stopServer(dnsServer);
|
||||||
|
dnsServer = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
@@ -193,13 +193,13 @@ async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
|||||||
try {
|
try {
|
||||||
// @ts-ignore - accessing private properties for emergency cleanup
|
// @ts-ignore - accessing private properties for emergency cleanup
|
||||||
if (server.httpsServer) {
|
if (server.httpsServer) {
|
||||||
server.httpsServer.close();
|
(server as any).httpsServer.close();
|
||||||
server.httpsServer = null;
|
(server as any).httpsServer = null;
|
||||||
}
|
}
|
||||||
// @ts-ignore - accessing private properties for emergency cleanup
|
// @ts-ignore - accessing private properties for emergency cleanup
|
||||||
if (server.udpServer) {
|
if (server.udpServer) {
|
||||||
server.udpServer.close();
|
(server as any).udpServer.close();
|
||||||
server.udpServer = null;
|
(server as any).udpServer = null;
|
||||||
}
|
}
|
||||||
} catch (forceError) {
|
} catch (forceError) {
|
||||||
console.log('Force cleanup error:', forceError.message || forceError);
|
console.log('Force cleanup error:', forceError.message || forceError);
|
||||||
|
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 = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartdns',
|
name: '@push.rocks/smartdns',
|
||||||
version: '7.4.0',
|
version: '7.6.0',
|
||||||
description: 'A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.'
|
description: 'A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.'
|
||||||
}
|
}
|
||||||
|
@@ -22,7 +22,13 @@ export const makeNodeProcessUseDnsProvider = (providerArg: TDnsProvider) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ISmartDnsConstructorOptions {}
|
export type TResolutionStrategy = 'doh' | 'system' | 'prefer-system';
|
||||||
|
|
||||||
|
export interface ISmartDnsConstructorOptions {
|
||||||
|
strategy?: TResolutionStrategy; // default: 'prefer-system'
|
||||||
|
allowDohFallback?: boolean; // allow fallback to DoH if system fails (default: true)
|
||||||
|
timeoutMs?: number; // optional per-query timeout
|
||||||
|
}
|
||||||
|
|
||||||
export interface IDnsJsonResponse {
|
export interface IDnsJsonResponse {
|
||||||
Status: number;
|
Status: number;
|
||||||
@@ -43,6 +49,9 @@ export interface IDnsJsonResponse {
|
|||||||
export class Smartdns {
|
export class Smartdns {
|
||||||
public dnsServerIp: string;
|
public dnsServerIp: string;
|
||||||
public dnsServerPort: number;
|
public dnsServerPort: number;
|
||||||
|
private strategy: TResolutionStrategy = 'prefer-system';
|
||||||
|
private allowDohFallback = true;
|
||||||
|
private timeoutMs: number | undefined;
|
||||||
|
|
||||||
public dnsTypeMap: { [key: string]: number } = {
|
public dnsTypeMap: { [key: string]: number } = {
|
||||||
A: 1,
|
A: 1,
|
||||||
@@ -55,7 +64,12 @@ export class Smartdns {
|
|||||||
/**
|
/**
|
||||||
* constructor for class dnsly
|
* constructor for class dnsly
|
||||||
*/
|
*/
|
||||||
constructor(optionsArg: ISmartDnsConstructorOptions) {}
|
constructor(optionsArg: ISmartDnsConstructorOptions) {
|
||||||
|
this.strategy = optionsArg?.strategy || 'prefer-system';
|
||||||
|
this.allowDohFallback =
|
||||||
|
optionsArg?.allowDohFallback === undefined ? true : optionsArg.allowDohFallback;
|
||||||
|
this.timeoutMs = optionsArg?.timeoutMs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check a dns record until it has propagated to Google DNS
|
* check a dns record until it has propagated to Google DNS
|
||||||
@@ -133,27 +147,111 @@ export class Smartdns {
|
|||||||
recordTypeArg: plugins.tsclass.network.TDnsRecordType,
|
recordTypeArg: plugins.tsclass.network.TDnsRecordType,
|
||||||
retriesCounterArg = 20
|
retriesCounterArg = 20
|
||||||
): Promise<plugins.tsclass.network.IDnsRecord[]> {
|
): Promise<plugins.tsclass.network.IDnsRecord[]> {
|
||||||
const requestUrl = `https://cloudflare-dns.com/dns-query?name=${recordNameArg}&type=${recordTypeArg}&do=1`;
|
const trySystem = async (): Promise<plugins.tsclass.network.IDnsRecord[]> => {
|
||||||
const returnArray: plugins.tsclass.network.IDnsRecord[] = [];
|
// Prefer dns.lookup for A/AAAA so hosts file and OS resolver are honored
|
||||||
const getResponseBody = async (counterArg = 0): Promise<IDnsJsonResponse> => {
|
if (recordTypeArg === 'A' || recordTypeArg === 'AAAA') {
|
||||||
const response = await plugins.smartrequest.request(requestUrl, {
|
const family = recordTypeArg === 'A' ? 4 : 6;
|
||||||
method: 'GET',
|
const addresses = await new Promise<{ address: string }[]>((resolve, reject) => {
|
||||||
headers: {
|
const timer = this.timeoutMs
|
||||||
accept: 'application/dns-json',
|
? setTimeout(() => reject(new Error('system lookup timeout')), this.timeoutMs)
|
||||||
},
|
: null;
|
||||||
});
|
plugins.dns.lookup(
|
||||||
const responseBody: IDnsJsonResponse = response.body;
|
recordNameArg,
|
||||||
if (responseBody?.Status !== 0 && counterArg < retriesCounterArg) {
|
{ family, all: true },
|
||||||
await plugins.smartdelay.delayFor(500);
|
(err, result) => {
|
||||||
return getResponseBody(counterArg + 1);
|
if (timer) clearTimeout(timer as any);
|
||||||
} else {
|
if (err) return reject(err);
|
||||||
return responseBody;
|
resolve(result || []);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return addresses.map((a) => ({
|
||||||
|
name: recordNameArg,
|
||||||
|
type: recordTypeArg,
|
||||||
|
dnsSecEnabled: false,
|
||||||
|
value: a.address,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
if (recordTypeArg === 'TXT') {
|
||||||
|
const records = await new Promise<string[][]>((resolve, reject) => {
|
||||||
|
const timer = this.timeoutMs
|
||||||
|
? setTimeout(() => reject(new Error('system resolveTxt timeout')), this.timeoutMs)
|
||||||
|
: null;
|
||||||
|
plugins.dns.resolveTxt(recordNameArg, (err, res) => {
|
||||||
|
if (timer) clearTimeout(timer as any);
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(res || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return records.map((chunks) => ({
|
||||||
|
name: recordNameArg,
|
||||||
|
type: 'TXT',
|
||||||
|
dnsSecEnabled: false,
|
||||||
|
value: chunks.join(''),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
};
|
};
|
||||||
const responseBody = await getResponseBody();
|
|
||||||
if (!responseBody.Answer || !typeof responseBody.Answer[Symbol.iterator]) {
|
const tryDoh = async (): Promise<plugins.tsclass.network.IDnsRecord[]> => {
|
||||||
|
const requestUrl = `https://cloudflare-dns.com/dns-query?name=${recordNameArg}&type=${recordTypeArg}&do=1`;
|
||||||
|
const returnArray: plugins.tsclass.network.IDnsRecord[] = [];
|
||||||
|
const getResponseBody = async (counterArg = 0): Promise<IDnsJsonResponse> => {
|
||||||
|
const response = await plugins.smartrequest.request(requestUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
accept: 'application/dns-json',
|
||||||
|
},
|
||||||
|
timeout: this.timeoutMs,
|
||||||
|
});
|
||||||
|
const responseBody: IDnsJsonResponse = response.body;
|
||||||
|
if (responseBody?.Status !== 0 && counterArg < retriesCounterArg) {
|
||||||
|
await plugins.smartdelay.delayFor(500);
|
||||||
|
return getResponseBody(counterArg + 1);
|
||||||
|
} else {
|
||||||
|
return responseBody;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const responseBody = await getResponseBody();
|
||||||
|
if (!responseBody || !responseBody.Answer || !typeof (responseBody.Answer as any)[Symbol.iterator]) {
|
||||||
|
return returnArray;
|
||||||
|
}
|
||||||
|
for (const dnsEntry of responseBody.Answer) {
|
||||||
|
if (typeof dnsEntry.data === 'string' && dnsEntry.data.startsWith('"') && dnsEntry.data.endsWith('"')) {
|
||||||
|
dnsEntry.data = dnsEntry.data.replace(/^\"(.*)\"$/, '$1');
|
||||||
|
}
|
||||||
|
if (dnsEntry.name.endsWith('.')) {
|
||||||
|
dnsEntry.name = dnsEntry.name.substring(0, dnsEntry.name.length - 1);
|
||||||
|
}
|
||||||
|
returnArray.push({
|
||||||
|
name: dnsEntry.name,
|
||||||
|
type: this.convertDnsTypeNumberToTypeName(dnsEntry.type),
|
||||||
|
dnsSecEnabled: !!responseBody.AD,
|
||||||
|
value: dnsEntry.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
return returnArray;
|
return returnArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.strategy === 'system') {
|
||||||
|
return await trySystem();
|
||||||
|
}
|
||||||
|
if (this.strategy === 'doh') {
|
||||||
|
return await tryDoh();
|
||||||
|
}
|
||||||
|
// prefer-system
|
||||||
|
try {
|
||||||
|
const sysRes = await trySystem();
|
||||||
|
if (sysRes.length > 0) return sysRes;
|
||||||
|
return this.allowDohFallback ? await tryDoh() : [];
|
||||||
|
} catch (err) {
|
||||||
|
return this.allowDohFallback ? await tryDoh() : [];
|
||||||
|
}
|
||||||
|
} catch (finalErr) {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for (const dnsEntry of responseBody.Answer) {
|
for (const dnsEntry of responseBody.Answer) {
|
||||||
if (dnsEntry.data.startsWith('"') && dnsEntry.data.endsWith('"')) {
|
if (dnsEntry.data.startsWith('"') && dnsEntry.data.endsWith('"')) {
|
||||||
dnsEntry.data = dnsEntry.data.replace(/^"(.*)"$/, '$1');
|
dnsEntry.data = dnsEntry.data.replace(/^"(.*)"$/, '$1');
|
||||||
|
@@ -13,6 +13,10 @@ export interface IDnsServerOptions {
|
|||||||
// New options for independent manual socket control
|
// New options for independent manual socket control
|
||||||
manualUdpMode?: boolean;
|
manualUdpMode?: boolean;
|
||||||
manualHttpsMode?: 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 {
|
export interface DnsAnswer {
|
||||||
@@ -559,11 +563,16 @@ export class DnsServer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const dnssecRequested = this.isDnssecRequested(request);
|
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) {
|
for (const question of request.questions) {
|
||||||
console.log(`Query for ${question.name} of type ${question.type}`);
|
console.log(`Query for ${question.name} of type ${question.type}`);
|
||||||
|
|
||||||
let answered = false;
|
let answered = false;
|
||||||
|
let shouldSignRrset = true; // skip DNSSEC signing for synthetic/local answers
|
||||||
|
const recordsForQuestion: DnsAnswer[] = [];
|
||||||
|
|
||||||
// Handle DNSKEY queries if DNSSEC is requested
|
// Handle DNSKEY queries if DNSSEC is requested
|
||||||
if (dnssecRequested && question.type === 'DNSKEY' && question.name === this.options.dnssecZone) {
|
if (dnssecRequested && question.type === 'DNSKEY' && question.name === this.options.dnssecZone) {
|
||||||
@@ -574,40 +583,92 @@ export class DnsServer {
|
|||||||
ttl: 3600,
|
ttl: 3600,
|
||||||
data: this.dnskeyRecord,
|
data: this.dnskeyRecord,
|
||||||
};
|
};
|
||||||
response.answers.push(dnskeyAnswer as plugins.dnsPacket.Answer);
|
recordsForQuestion.push(dnskeyAnswer);
|
||||||
|
|
||||||
// Sign the DNSKEY RRset
|
|
||||||
const rrsig = this.generateRRSIG('DNSKEY', [dnskeyAnswer], question.name);
|
|
||||||
response.answers.push(rrsig as plugins.dnsPacket.Answer);
|
|
||||||
|
|
||||||
answered = true;
|
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) {
|
// Add records to response and group by type for DNSSEC
|
||||||
if (
|
if (recordsForQuestion.length > 0) {
|
||||||
plugins.minimatch.minimatch(question.name, handlerEntry.domainPattern) &&
|
for (const record of recordsForQuestion) {
|
||||||
handlerEntry.recordTypes.includes(question.type)
|
response.answers.push(record as plugins.dnsPacket.Answer);
|
||||||
) {
|
}
|
||||||
const answer = handlerEntry.handler(question);
|
|
||||||
if (answer) {
|
// Group records by type for DNSSEC signing
|
||||||
// Ensure the answer has ttl and class
|
if (dnssecRequested && shouldSignRrset) {
|
||||||
const dnsAnswer: DnsAnswer = {
|
const rrsetKey = `${question.name}:${question.type}`;
|
||||||
...answer,
|
rrsetMap.set(rrsetKey, recordsForQuestion);
|
||||||
ttl: answer.ttl || 300,
|
|
||||||
class: answer.class || 'IN',
|
|
||||||
};
|
|
||||||
response.answers.push(dnsAnswer as plugins.dnsPacket.Answer);
|
|
||||||
|
|
||||||
if (dnssecRequested) {
|
|
||||||
// Sign the answer RRset
|
|
||||||
const rrsig = this.generateRRSIG(question.type, [dnsAnswer], question.name);
|
|
||||||
response.answers.push(rrsig as plugins.dnsPacket.Answer);
|
|
||||||
}
|
|
||||||
|
|
||||||
answered = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,7 +681,7 @@ export class DnsServer {
|
|||||||
class: 'IN',
|
class: 'IN',
|
||||||
ttl: 3600,
|
ttl: 3600,
|
||||||
data: {
|
data: {
|
||||||
mname: `ns1.${this.options.dnssecZone}`,
|
mname: this.options.primaryNameserver || `ns1.${this.options.dnssecZone}`,
|
||||||
rname: `hostmaster.${this.options.dnssecZone}`,
|
rname: `hostmaster.${this.options.dnssecZone}`,
|
||||||
serial: Math.floor(Date.now() / 1000),
|
serial: Math.floor(Date.now() / 1000),
|
||||||
refresh: 3600,
|
refresh: 3600,
|
||||||
@@ -630,6 +691,22 @@ export class DnsServer {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
response.answers.push(soaAnswer as plugins.dnsPacket.Answer);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,6 +744,17 @@ export class DnsServer {
|
|||||||
|
|
||||||
// Sign the RRset
|
// Sign the RRset
|
||||||
const signature = this.dnsSec.signData(rrsetBuffer);
|
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
|
// Construct the RRSIG record
|
||||||
const rrsig: DnsAnswer = {
|
const rrsig: DnsAnswer = {
|
||||||
@@ -675,15 +763,15 @@ export class DnsServer {
|
|||||||
class: 'IN',
|
class: 'IN',
|
||||||
ttl,
|
ttl,
|
||||||
data: {
|
data: {
|
||||||
typeCovered: type, // Changed to type string
|
typeCovered: type, // dns-packet expects the string type
|
||||||
algorithm,
|
algorithm,
|
||||||
labels: name.split('.').length - 1,
|
labels: name.split('.').filter(l => l.length > 0).length, // Fix label count
|
||||||
originalTTL: ttl,
|
originalTTL: ttl,
|
||||||
expiration,
|
expiration,
|
||||||
inception,
|
inception,
|
||||||
keyTag,
|
keyTag,
|
||||||
signerName,
|
signersName: signerName || this.options.dnssecZone, // Note: signersName with 's'
|
||||||
signature: signature,
|
signature: signature || Buffer.alloc(0), // Fallback to empty buffer
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -756,10 +844,31 @@ export class DnsServer {
|
|||||||
Buffer.from([dnskeyData.algorithm]),
|
Buffer.from([dnskeyData.algorithm]),
|
||||||
dnskeyData.key,
|
dnskeyData.key,
|
||||||
]);
|
]);
|
||||||
|
case 'NS':
|
||||||
|
// NS records contain domain names
|
||||||
|
return this.nameToBuffer(data);
|
||||||
case 'SOA':
|
case 'SOA':
|
||||||
// Implement SOA record serialization if needed
|
// Implement SOA record serialization according to RFC 1035
|
||||||
// For now, return an empty buffer or handle as needed
|
const mname = this.nameToBuffer(data.mname);
|
||||||
return Buffer.alloc(0);
|
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
|
// Add cases for other record types as needed
|
||||||
default:
|
default:
|
||||||
throw new Error(`Serialization for record type ${type} is not implemented.`);
|
throw new Error(`Serialization for record type ${type} is not implemented.`);
|
||||||
@@ -964,7 +1073,9 @@ export class DnsServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private nameToBuffer(name: string): Buffer {
|
private nameToBuffer(name: string): Buffer {
|
||||||
const labels = name.split('.');
|
// Trim trailing dot to avoid double-root
|
||||||
|
const trimmed = name.endsWith('.') ? name.slice(0, -1) : name;
|
||||||
|
const labels = trimmed.split('.').filter(l => l.length > 0);
|
||||||
const buffers = labels.map(label => {
|
const buffers = labels.map(label => {
|
||||||
const len = Buffer.byteLength(label, 'utf8');
|
const len = Buffer.byteLength(label, 'utf8');
|
||||||
const buf = Buffer.alloc(1 + len);
|
const buf = Buffer.alloc(1 + len);
|
||||||
@@ -972,6 +1083,7 @@ export class DnsServer {
|
|||||||
buf.write(label, 1);
|
buf.write(label, 1);
|
||||||
return buf;
|
return buf;
|
||||||
});
|
});
|
||||||
return Buffer.concat([...buffers, Buffer.from([0])]); // Add root label
|
// Append exactly one root label
|
||||||
|
return Buffer.concat([...buffers, Buffer.from([0])]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user