Compare commits

...

15 Commits

Author SHA1 Message Date
56a33dd7ae 7.5.0 2025-06-01 20:53:22 +00:00
9e5fae055f feat(dnssec): Add MX record DNSSEC support for proper serialization and authentication of mail exchange records 2025-06-01 20:53:22 +00:00
afdd6a6074 7.4.7 2025-05-30 19:49:34 +00:00
3d06131e04 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. 2025-05-30 19:49:34 +00:00
1811ebd4d4 7.4.6 2025-05-30 19:28:54 +00:00
e7ace9b596 7.4.5 2025-05-30 19:28:22 +00:00
f6175d1f2b 7.4.4
fix(dnsserver): Fix SOA record timeout issue by correcting RRSIG field formatting

- Fixed RRSIG generation by using correct field name 'signersName' (not 'signerName')
- Fixed label count calculation in RRSIG by filtering empty strings
- Added SOA records to DNSSEC signing map for proper RRSIG generation
- Added error logging and fallback values for RRSIG generation robustness
- Updated test expectations to match corrected DNSSEC RRset signing behavior
- Added comprehensive SOA test coverage including timeout, debug, and simple test scenarios
2025-05-30 19:27:37 +00:00
d67fbc87e2 7.4.3 2025-05-30 18:27:28 +00:00
b87cbbee5c feat(dnsserver): Enhance DNSSEC RRset signing and add configurable primary nameserver
- Fix DNSSEC to properly sign entire RRsets together instead of individual records
- Implement proper SOA record serialization according to RFC 1035
- Add primaryNameserver option to IDnsServerOptions for customizable SOA mname field
- Add comprehensive tests for DNSSEC RRset signing and SOA record handling
- Update documentation with v7.4.3 improvements

Co-Authored-By: User <user@example.com>
2025-05-30 18:20:55 +00:00
4e37bc9bc0 7.4.2 2025-05-30 17:09:02 +00:00
2b97dffb47 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. 2025-05-30 17:09:02 +00:00
e7cb0921fc 7.4.1 2025-05-28 19:55:01 +00:00
0f8953fc1d fix(test/server): Fix force cleanup in DNS server tests by casting server properties before closing sockets 2025-05-28 19:55:01 +00:00
1185ea67d4 7.4.0 2025-05-28 19:26:52 +00:00
b187da507b feat(manual socket handling): Add comprehensive manual socket handling documentation for advanced DNS server use cases 2025-05-28 19:26:52 +00:00
18 changed files with 3300 additions and 150 deletions

View File

@ -1,5 +1,56 @@
# Changelog
## 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)
Add comprehensive manual socket handling documentation for advanced DNS server use cases
- Introduced detailed examples for configuring manual UDP and HTTPS socket handling
- Provided sample code for load balancing, clustering, custom transport protocols, and multi-interface binding
- Updated performance and best practices sections to reflect manual socket handling benefits
## 2025-05-28 - 7.3.0 - feat(dnsserver)
Add manual socket mode support to enable external socket control for the DNS server.

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartdns",
"version": "7.3.0",
"version": "7.5.0",
"private": false,
"description": "A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.",
"exports": {

View File

@ -54,9 +54,17 @@ The smartdns library is structured into three main modules:
### Handler System:
- Pattern-based domain matching using minimatch
- 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
- 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
- `dns-packet`: DNS packet encoding/decoding (wire format)
@ -95,3 +103,20 @@ The test suite demonstrates:
- DoH (DNS-over-HTTPS) provides both privacy and integrity
- Let's Encrypt integration requires proper domain authorization
- 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)

252
readme.md
View File

@ -198,7 +198,8 @@ const secureServer = new DnsServer({
httpsCert: 'path/to/cert.pem',
dnssecZone: 'example.com',
udpBindInterface: '127.0.0.1', // Bind UDP to localhost only
httpsBindInterface: '127.0.0.1' // Bind HTTPS to localhost only
httpsBindInterface: '127.0.0.1', // Bind HTTPS to localhost only
primaryNameserver: 'ns1.example.com' // Optional: primary nameserver for SOA records (defaults to ns1.{dnssecZone})
});
// Register a handler for all subdomains of example.com
@ -224,6 +225,35 @@ await dnsServer.start();
console.log('DNS Server started!');
```
### SOA Records and Primary Nameserver
The DNS server automatically generates SOA (Start of Authority) records for zones when no specific handler matches a query. The SOA record contains important zone metadata including the primary nameserver.
```typescript
const dnsServer = new DnsServer({
udpPort: 53,
httpsPort: 443,
httpsKey: 'path/to/key.pem',
httpsCert: 'path/to/cert.pem',
dnssecZone: 'example.com',
primaryNameserver: 'ns1.example.com' // Specify your actual primary nameserver
});
// Without primaryNameserver, the SOA mname defaults to 'ns1.{dnssecZone}'
// In this case, it would be 'ns1.example.com'
// The automatic SOA record includes:
// - mname: Primary nameserver (from primaryNameserver option)
// - rname: Responsible person email (hostmaster.{dnssecZone})
// - serial: Unix timestamp
// - refresh: 3600 (1 hour)
// - retry: 600 (10 minutes)
// - expire: 604800 (7 days)
// - minimum: 86400 (1 day)
```
**Important**: Even if you have multiple nameservers (NS records), only one is designated as the primary in the SOA record. All authoritative nameservers should return the same SOA record.
### DNSSEC Support
The DNS server includes comprehensive DNSSEC support with automatic key generation and record signing:
@ -306,6 +336,222 @@ await dnsServer.start();
console.log('DNS Server with Let\'s Encrypt SSL started!');
```
### Manual Socket Handling
The DNS server supports manual socket handling for advanced use cases like clustering, load balancing, and custom transport implementations. You can control UDP and HTTPS socket handling independently.
#### Configuration Options
```typescript
export interface IDnsServerOptions {
httpsKey: string; // Path or content of HTTPS private key
httpsCert: string; // Path or content of HTTPS certificate
httpsPort: number; // Port for DNS-over-HTTPS
udpPort: number; // Port for standard UDP DNS
dnssecZone: string; // Zone name for DNSSEC signing
udpBindInterface?: string; // IP address to bind UDP socket (default: '0.0.0.0')
httpsBindInterface?: string; // IP address to bind HTTPS server (default: '0.0.0.0')
manualUdpMode?: boolean; // Handle UDP sockets manually
manualHttpsMode?: boolean; // Handle HTTPS sockets manually
primaryNameserver?: string; // Primary nameserver for SOA records (default: 'ns1.{dnssecZone}')
}
```
#### Basic Manual Socket Usage
```typescript
import { DnsServer } from '@push.rocks/smartdns/server';
import * as dgram from 'dgram';
import * as net from 'net';
// Create server with manual UDP mode
const dnsServer = new DnsServer({
httpsKey: '...',
httpsCert: '...',
httpsPort: 853,
udpPort: 53,
dnssecZone: 'example.com',
manualUdpMode: true // UDP manual, HTTPS automatic
});
await dnsServer.start(); // HTTPS binds, UDP doesn't
// Create your own UDP socket
const udpSocket = dgram.createSocket('udp4');
// Handle incoming UDP messages
udpSocket.on('message', (msg, rinfo) => {
dnsServer.handleUdpMessage(msg, rinfo, (response, responseRinfo) => {
// Send response using your socket
udpSocket.send(response, responseRinfo.port, responseRinfo.address);
});
});
// Bind to custom port or multiple interfaces
udpSocket.bind(5353, '0.0.0.0');
```
#### Manual HTTPS Socket Handling
```typescript
// Create server with manual HTTPS mode
const dnsServer = new DnsServer({
httpsKey: '...',
httpsCert: '...',
httpsPort: 853,
udpPort: 53,
dnssecZone: 'example.com',
manualHttpsMode: true // HTTPS manual, UDP automatic
});
await dnsServer.start(); // UDP binds, HTTPS doesn't
// Create your own TCP server
const tcpServer = net.createServer((socket) => {
// Pass TCP sockets to DNS server
dnsServer.handleHttpsSocket(socket);
});
tcpServer.listen(8853, '0.0.0.0');
```
#### Full Manual Mode
Control both protocols manually for complete flexibility:
```typescript
const dnsServer = new DnsServer({
httpsKey: '...',
httpsCert: '...',
httpsPort: 853,
udpPort: 53,
dnssecZone: 'example.com',
manualUdpMode: true,
manualHttpsMode: true
});
await dnsServer.start(); // Neither protocol binds
// Set up your own socket handling for both protocols
// Perfect for custom routing, load balancing, or clustering
```
#### Advanced Use Cases
##### Load Balancing Across Multiple UDP Sockets
```typescript
// Create multiple UDP sockets for different CPU cores
const sockets = [];
const numCPUs = require('os').cpus().length;
for (let i = 0; i < numCPUs; i++) {
const socket = dgram.createSocket({
type: 'udp4',
reuseAddr: true // Allow multiple sockets on same port
});
socket.on('message', (msg, rinfo) => {
dnsServer.handleUdpMessage(msg, rinfo, (response, rinfo) => {
socket.send(response, rinfo.port, rinfo.address);
});
});
socket.bind(53);
sockets.push(socket);
}
```
##### Clustering with Worker Processes
```typescript
import cluster from 'cluster';
import { DnsServer } from '@push.rocks/smartdns/server';
if (cluster.isPrimary) {
// Master process accepts connections
const server = net.createServer({ pauseOnConnect: true });
// Distribute connections to workers
server.on('connection', (socket) => {
const worker = getNextWorker(); // Round-robin or custom logic
worker.send('socket', socket);
});
server.listen(853);
} else {
// Worker process handles DNS
const dnsServer = new DnsServer({
httpsKey: '...',
httpsCert: '...',
httpsPort: 853,
udpPort: 53,
dnssecZone: 'example.com',
manualHttpsMode: true
});
process.on('message', (msg, socket) => {
if (msg === 'socket') {
dnsServer.handleHttpsSocket(socket);
}
});
await dnsServer.start();
}
```
##### Custom Transport Protocol
```typescript
// Use DNS server with custom transport (e.g., WebSocket)
import WebSocket from 'ws';
const wss = new WebSocket.Server({ port: 8080 });
const dnsServer = new DnsServer({
httpsKey: '...',
httpsCert: '...',
httpsPort: 853,
udpPort: 53,
dnssecZone: 'example.com',
manualUdpMode: true,
manualHttpsMode: true
});
await dnsServer.start();
wss.on('connection', (ws) => {
ws.on('message', (data) => {
// Process DNS query from WebSocket
const response = dnsServer.processRawDnsPacket(Buffer.from(data));
ws.send(response);
});
});
```
##### Multi-Interface Binding
```typescript
// Bind to multiple network interfaces manually
const interfaces = [
{ address: '192.168.1.100', type: 'udp4' },
{ address: '10.0.0.50', type: 'udp4' },
{ address: '::1', type: 'udp6' }
];
interfaces.forEach(({ address, type }) => {
const socket = dgram.createSocket(type);
socket.on('message', (msg, rinfo) => {
console.log(`Query received on ${address}`);
dnsServer.handleUdpMessage(msg, rinfo, (response, rinfo) => {
socket.send(response, rinfo.port, rinfo.address);
});
});
socket.bind(53, address);
});
```
### Handling Different Protocols
#### UDP DNS Server
@ -573,6 +819,7 @@ await tap.start();
5. **Monitoring**: Log DNS queries for debugging and analytics
6. **Rate Limiting**: Implement rate limiting for public DNS servers
7. **Caching**: Respect TTL values and implement proper caching
8. **Manual Sockets**: Use manual socket handling for clustering and load balancing
### Performance Considerations
@ -580,6 +827,7 @@ await tap.start();
- The DNS server handles concurrent UDP and HTTPS requests efficiently
- DNSSEC signatures are generated on-demand to reduce memory usage
- Pattern matching uses caching for improved performance
- Manual socket handling enables horizontal scaling across CPU cores
### Security Considerations
@ -589,6 +837,8 @@ await tap.start();
- Implement access controls for DNS server handlers
- Use Let's Encrypt for automatic SSL certificate management
- Never expose internal network information through DNS
- Bind to specific interfaces in production environments
- Use manual socket handling for custom security layers
This comprehensive library provides everything needed for both DNS client operations and running production-grade DNS servers with modern security features in TypeScript.

View File

@ -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
View 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
View 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
View 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();

View 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();

View 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();

View File

@ -0,0 +1,419 @@
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 demonstrate the current limitation with 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));
// CURRENT BEHAVIOR: Only returns 1 NS record due to the break statement
expect(dnsResponse.answers.length).toEqual(1);
expect((dnsResponse.answers[0] as any).data).toEqual('ns1.example.com');
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('should demonstrate the limitation with multiple A records (round-robin)', 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));
// CURRENT BEHAVIOR: Only returns 1 A record, preventing round-robin DNS
expect(dnsResponse.answers.length).toEqual(1);
expect((dnsResponse.answers[0] as any).data).toEqual('10.0.0.1');
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('should demonstrate the limitation with 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));
// CURRENT BEHAVIOR: Only returns 1 TXT record instead of all 3
expect(dnsResponse.answers.length).toEqual(1);
expect((dnsResponse.answers[0] as any).data[0]).toInclude('spf1');
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('should show the current workaround pattern', 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',
});
// WORKAROUND: Create an array to store NS records and return them from a single handler
const nsRecords = ['ns1.example.com', 'ns2.example.com'];
let nsIndex = 0;
// This workaround still doesn't solve the problem because only one handler executes
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 workaround rotates between records but still only returns one at a time
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();

View File

@ -193,13 +193,13 @@ async function stopServer(server: smartdns.DnsServer | null | undefined) {
try {
// @ts-ignore - accessing private properties for emergency cleanup
if (server.httpsServer) {
server.httpsServer.close();
server.httpsServer = null;
(server as any).httpsServer.close();
(server as any).httpsServer = null;
}
// @ts-ignore - accessing private properties for emergency cleanup
if (server.udpServer) {
server.udpServer.close();
server.udpServer = null;
(server as any).udpServer.close();
(server as any).udpServer = null;
}
} catch (forceError) {
console.log('Force cleanup error:', forceError.message || forceError);

269
test/test.soa.debug.ts Normal file
View 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
View 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
View 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
View 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();

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartdns',
version: '7.3.0',
version: '7.5.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.'
}

View File

@ -13,6 +13,8 @@ export interface IDnsServerOptions {
// New options for independent manual socket control
manualUdpMode?: boolean;
manualHttpsMode?: boolean;
// Primary nameserver for SOA records (defaults to ns1.{dnssecZone})
primaryNameserver?: string;
}
export interface DnsAnswer {
@ -560,10 +562,14 @@ export class DnsServer {
const dnssecRequested = this.isDnssecRequested(request);
// Map to group records by type for proper DNSSEC RRset signing
const rrsetMap = new Map<string, DnsAnswer[]>();
for (const question of request.questions) {
console.log(`Query for ${question.name} of type ${question.type}`);
let answered = false;
const recordsForQuestion: DnsAnswer[] = [];
// Handle DNSKEY queries if DNSSEC is requested
if (dnssecRequested && question.type === 'DNSKEY' && question.name === this.options.dnssecZone) {
@ -574,16 +580,10 @@ export class DnsServer {
ttl: 3600,
data: this.dnskeyRecord,
};
response.answers.push(dnskeyAnswer as plugins.dnsPacket.Answer);
// Sign the DNSKEY RRset
const rrsig = this.generateRRSIG('DNSKEY', [dnskeyAnswer], question.name);
response.answers.push(rrsig as plugins.dnsPacket.Answer);
recordsForQuestion.push(dnskeyAnswer);
answered = true;
continue;
}
} else {
// Collect all matching records from handlers
for (const handlerEntry of this.handlers) {
if (
plugins.minimatch.minimatch(question.name, handlerEntry.domainPattern) &&
@ -597,19 +597,26 @@ export class DnsServer {
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);
}
recordsForQuestion.push(dnsAnswer);
answered = true;
break;
// Continue processing other handlers to allow multiple records
}
}
}
}
// Add records to response and group by type for DNSSEC
if (recordsForQuestion.length > 0) {
for (const record of recordsForQuestion) {
response.answers.push(record as plugins.dnsPacket.Answer);
}
// Group records by type for DNSSEC signing
if (dnssecRequested) {
const rrsetKey = `${question.name}:${question.type}`;
rrsetMap.set(rrsetKey, recordsForQuestion);
}
}
if (!answered) {
console.log(`No handler found for ${question.name} of type ${question.type}`);
@ -620,7 +627,7 @@ export class DnsServer {
class: 'IN',
ttl: 3600,
data: {
mname: `ns1.${this.options.dnssecZone}`,
mname: this.options.primaryNameserver || `ns1.${this.options.dnssecZone}`,
rname: `hostmaster.${this.options.dnssecZone}`,
serial: Math.floor(Date.now() / 1000),
refresh: 3600,
@ -630,6 +637,22 @@ export class DnsServer {
},
};
response.answers.push(soaAnswer as plugins.dnsPacket.Answer);
// Add SOA record to DNSSEC signing map if DNSSEC is requested
if (dnssecRequested) {
const soaKey = `${question.name}:SOA`;
rrsetMap.set(soaKey, [soaAnswer]);
}
}
}
// Sign RRsets if DNSSEC is requested
if (dnssecRequested) {
for (const [key, rrset] of rrsetMap) {
const [name, type] = key.split(':');
// Sign the entire RRset together
const rrsig = this.generateRRSIG(type, rrset, name);
response.answers.push(rrsig as plugins.dnsPacket.Answer);
}
}
@ -668,6 +691,17 @@ export class DnsServer {
// Sign the RRset
const signature = this.dnsSec.signData(rrsetBuffer);
// Ensure all fields are defined
if (!signerName || !signature) {
console.error('RRSIG generation error - missing fields:', {
signerName,
signature: signature ? 'present' : 'missing',
algorithm,
keyTag,
type
});
}
// Construct the RRSIG record
const rrsig: DnsAnswer = {
name,
@ -675,15 +709,15 @@ export class DnsServer {
class: 'IN',
ttl,
data: {
typeCovered: type, // Changed to type string
typeCovered: type, // dns-packet expects the string type
algorithm,
labels: name.split('.').length - 1,
labels: name.split('.').filter(l => l.length > 0).length, // Fix label count
originalTTL: ttl,
expiration,
inception,
keyTag,
signerName,
signature: signature,
signersName: signerName || this.options.dnssecZone, // Note: signersName with 's'
signature: signature || Buffer.alloc(0), // Fallback to empty buffer
},
};
@ -756,10 +790,31 @@ export class DnsServer {
Buffer.from([dnskeyData.algorithm]),
dnskeyData.key,
]);
case 'NS':
// NS records contain domain names
return this.nameToBuffer(data);
case 'SOA':
// Implement SOA record serialization if needed
// For now, return an empty buffer or handle as needed
return Buffer.alloc(0);
// Implement SOA record serialization according to RFC 1035
const mname = this.nameToBuffer(data.mname);
const rname = this.nameToBuffer(data.rname);
const serial = Buffer.alloc(4);
serial.writeUInt32BE(data.serial, 0);
const refresh = Buffer.alloc(4);
refresh.writeUInt32BE(data.refresh, 0);
const retry = Buffer.alloc(4);
retry.writeUInt32BE(data.retry, 0);
const expire = Buffer.alloc(4);
expire.writeUInt32BE(data.expire, 0);
const minimum = Buffer.alloc(4);
minimum.writeUInt32BE(data.minimum, 0);
return Buffer.concat([mname, rname, serial, refresh, retry, expire, minimum]);
case 'MX':
// MX records contain preference (16-bit) and exchange (domain name)
const preference = Buffer.alloc(2);
preference.writeUInt16BE(data.preference, 0);
const exchange = this.nameToBuffer(data.exchange);
return Buffer.concat([preference, exchange]);
// Add cases for other record types as needed
default:
throw new Error(`Serialization for record type ${type} is not implemented.`);