Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
56a33dd7ae | |||
9e5fae055f | |||
afdd6a6074 | |||
3d06131e04 | |||
1811ebd4d4 | |||
e7ace9b596 | |||
f6175d1f2b | |||
d67fbc87e2 | |||
b87cbbee5c |
31
changelog.md
31
changelog.md
@ -1,5 +1,36 @@
|
||||
# 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.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartdns",
|
||||
"version": "7.4.2",
|
||||
"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": {
|
||||
|
@ -104,8 +104,19 @@ The test suite demonstrates:
|
||||
- Let's Encrypt integration requires proper domain authorization
|
||||
- Handler patterns should be carefully designed to avoid open resolvers
|
||||
|
||||
## Known Issues
|
||||
## Recent Improvements (v7.4.3)
|
||||
|
||||
1. **DNSSEC RRSIG Generation**: When multiple records of the same type are returned, DNSSEC signing may encounter issues with the current implementation
|
||||
2. **SOA Record Prefix**: The server hardcodes 'ns1.' prefix for SOA mname field which may not match actual nameserver names
|
||||
3. **Handler Deduplication**: If the same handler is registered multiple times, it will contribute duplicate records
|
||||
1. **DNSSEC RRset Signing**: Fixed to properly sign entire RRsets together instead of individual records
|
||||
2. **SOA Record Serialization**: Implemented proper SOA record encoding for DNSSEC compatibility
|
||||
3. **Configurable Primary Nameserver**: Added `primaryNameserver` option to customize SOA mname field
|
||||
|
||||
## Recent Improvements (v7.4.8)
|
||||
|
||||
1. **MX Record DNSSEC Support**: Implemented MX record serialization for DNSSEC signing
|
||||
- MX records consist of a 16-bit preference value followed by the exchange domain name
|
||||
- Properly serializes both components for DNSSEC signature generation
|
||||
- Enables mail exchange records to be authenticated with DNSSEC
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Handler Deduplication**: If the same handler is registered multiple times, it will contribute duplicate records (this may be desired behavior for some use cases)
|
41
readme.md
41
readme.md
@ -198,7 +198,8 @@ const secureServer = new DnsServer({
|
||||
httpsCert: 'path/to/cert.pem',
|
||||
dnssecZone: 'example.com',
|
||||
udpBindInterface: '127.0.0.1', // Bind UDP to localhost only
|
||||
httpsBindInterface: '127.0.0.1' // Bind HTTPS to localhost only
|
||||
httpsBindInterface: '127.0.0.1', // Bind HTTPS to localhost only
|
||||
primaryNameserver: 'ns1.example.com' // Optional: primary nameserver for SOA records (defaults to ns1.{dnssecZone})
|
||||
});
|
||||
|
||||
// Register a handler for all subdomains of example.com
|
||||
@ -224,6 +225,35 @@ await dnsServer.start();
|
||||
console.log('DNS Server started!');
|
||||
```
|
||||
|
||||
### SOA Records and Primary Nameserver
|
||||
|
||||
The DNS server automatically generates SOA (Start of Authority) records for zones when no specific handler matches a query. The SOA record contains important zone metadata including the primary nameserver.
|
||||
|
||||
```typescript
|
||||
const dnsServer = new DnsServer({
|
||||
udpPort: 53,
|
||||
httpsPort: 443,
|
||||
httpsKey: 'path/to/key.pem',
|
||||
httpsCert: 'path/to/cert.pem',
|
||||
dnssecZone: 'example.com',
|
||||
primaryNameserver: 'ns1.example.com' // Specify your actual primary nameserver
|
||||
});
|
||||
|
||||
// Without primaryNameserver, the SOA mname defaults to 'ns1.{dnssecZone}'
|
||||
// In this case, it would be 'ns1.example.com'
|
||||
|
||||
// The automatic SOA record includes:
|
||||
// - mname: Primary nameserver (from primaryNameserver option)
|
||||
// - rname: Responsible person email (hostmaster.{dnssecZone})
|
||||
// - serial: Unix timestamp
|
||||
// - refresh: 3600 (1 hour)
|
||||
// - retry: 600 (10 minutes)
|
||||
// - expire: 604800 (7 days)
|
||||
// - minimum: 86400 (1 day)
|
||||
```
|
||||
|
||||
**Important**: Even if you have multiple nameservers (NS records), only one is designated as the primary in the SOA record. All authoritative nameservers should return the same SOA record.
|
||||
|
||||
### DNSSEC Support
|
||||
|
||||
The DNS server includes comprehensive DNSSEC support with automatic key generation and record signing:
|
||||
@ -314,9 +344,16 @@ The DNS server supports manual socket handling for advanced use cases like clust
|
||||
|
||||
```typescript
|
||||
export interface IDnsServerOptions {
|
||||
// ... standard options ...
|
||||
httpsKey: string; // Path or content of HTTPS private key
|
||||
httpsCert: string; // Path or content of HTTPS certificate
|
||||
httpsPort: number; // Port for DNS-over-HTTPS
|
||||
udpPort: number; // Port for standard UDP DNS
|
||||
dnssecZone: string; // Zone name for DNSSEC signing
|
||||
udpBindInterface?: string; // IP address to bind UDP socket (default: '0.0.0.0')
|
||||
httpsBindInterface?: string; // IP address to bind HTTPS server (default: '0.0.0.0')
|
||||
manualUdpMode?: boolean; // Handle UDP sockets manually
|
||||
manualHttpsMode?: boolean; // Handle HTTPS sockets manually
|
||||
primaryNameserver?: string; // Primary nameserver for SOA records (default: 'ns1.{dnssecZone}')
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -1,97 +0,0 @@
|
||||
# SmartDNS Improvement Plan
|
||||
|
||||
Command to reread CLAUDE.md: `cat /home/philkunz/.claude/CLAUDE.md`
|
||||
|
||||
## Critical Issue: Support Multiple DNS Records of Same Type
|
||||
|
||||
### Current Status: Planning
|
||||
**Priority: HIGH** - This issue blocks proper DNS server operation and domain registration
|
||||
|
||||
### Problem Summary
|
||||
The DNS server currently exits after finding the first matching handler for a query, preventing it from serving multiple records of the same type (e.g., multiple NS records, multiple A records for round-robin, multiple TXT records).
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
#### Phase 1: Analysis and Testing ✅ COMPLETED
|
||||
- [x] Create comprehensive test cases demonstrating the issue
|
||||
- [x] Test with multiple NS records scenario
|
||||
- [x] Test with multiple A records (round-robin) scenario
|
||||
- [x] Test with multiple TXT records scenario
|
||||
- [x] Document current behavior vs expected behavior
|
||||
|
||||
#### Phase 2: Core Fix Implementation ✅ COMPLETED
|
||||
- [x] Remove the `break` statement in `processDnsRequest` method (line 609)
|
||||
- [x] Ensure all matching handlers are processed
|
||||
- [x] Accumulate all answers from matching handlers
|
||||
- [x] Add NS record serialization for DNSSEC support
|
||||
|
||||
#### Phase 3: Handler Interface Enhancement (Optional)
|
||||
- [ ] Consider allowing handlers to return arrays of records
|
||||
- [ ] Update `IDnsHandler` interface to support `DnsAnswer | DnsAnswer[] | null`
|
||||
- [ ] Update processing logic to handle array responses
|
||||
- [ ] Maintain backward compatibility with existing handlers
|
||||
|
||||
#### Phase 4: Testing and Validation
|
||||
- [ ] Test multiple NS records return correctly
|
||||
- [ ] Test round-robin DNS with multiple A records
|
||||
- [ ] Test multiple TXT records (SPF + DKIM + verification)
|
||||
- [ ] Test DNSSEC signatures for multiple records
|
||||
- [ ] Verify no regression in single-record scenarios
|
||||
|
||||
#### Phase 5: Documentation and Examples
|
||||
- [ ] Update documentation with multiple record examples
|
||||
- [ ] Add example for registering multiple NS records
|
||||
- [ ] Add example for round-robin DNS setup
|
||||
- [ ] Document best practices for handler registration
|
||||
|
||||
### Technical Details
|
||||
|
||||
#### Current Code Issue (ts_server/classes.dnsserver.ts:609)
|
||||
```typescript
|
||||
answered = true;
|
||||
break; // <-- This prevents multiple handlers from contributing answers
|
||||
```
|
||||
|
||||
#### Proposed Fix
|
||||
```typescript
|
||||
answered = true;
|
||||
// Continue processing other handlers instead of breaking
|
||||
```
|
||||
|
||||
### Success Criteria
|
||||
- DNS queries return ALL matching records from ALL matching handlers
|
||||
- Domain registration with multiple NS records succeeds
|
||||
- Round-robin DNS works with multiple A records
|
||||
- Multiple TXT records can be served for the same domain
|
||||
- DNSSEC signatures are properly generated for all returned records
|
||||
|
||||
### Implementation Summary
|
||||
|
||||
#### What Was Fixed
|
||||
1. **Core Issue Resolved**: Removed the `break` statement at line 609 in `processDnsRequest` that was preventing multiple handlers from contributing DNS answers
|
||||
2. **NS Record Serialization**: Added NS record type support in `serializeRData` method for DNSSEC compatibility
|
||||
3. **Result**: DNS server now correctly returns multiple records of the same type from different handlers
|
||||
|
||||
#### Test Results
|
||||
- ✅ Multiple NS records now work (2+ nameservers returned)
|
||||
- ✅ Round-robin DNS with multiple A records works
|
||||
- ✅ Multiple TXT records (SPF, DKIM, verification) work
|
||||
- ⚠️ DNSSEC RRSIG generation needs additional fixes for multiple record scenarios
|
||||
|
||||
#### Code Changes
|
||||
```typescript
|
||||
// Before (line 609):
|
||||
answered = true;
|
||||
break; // This was preventing multiple handlers from running
|
||||
|
||||
// After:
|
||||
answered = true;
|
||||
// Continue processing other handlers to allow multiple records
|
||||
```
|
||||
|
||||
### Additional Improvements to Consider
|
||||
1. Fix DNSSEC RRSIG generation for multiple records
|
||||
2. Fix SOA record timeout issues
|
||||
3. Make DNSSEC zone prefix configurable (remove hardcoded 'ns1.')
|
||||
4. Improve error handling for edge cases
|
||||
5. Consider handler interface enhancement to return arrays
|
123
test/example.primaryns.ts
Normal file
123
test/example.primaryns.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
// Example: Using custom primary nameserver
|
||||
async function exampleCustomNameserver() {
|
||||
const dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: 'your-https-key',
|
||||
httpsCert: 'your-https-cert',
|
||||
httpsPort: 8443,
|
||||
udpPort: 8053,
|
||||
dnssecZone: 'example.com',
|
||||
// Custom primary nameserver for SOA records
|
||||
primaryNameserver: 'ns-primary.example.com',
|
||||
});
|
||||
|
||||
// Register some handlers
|
||||
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'NS',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: 'ns-primary.example.com',
|
||||
};
|
||||
});
|
||||
|
||||
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'NS',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: 'ns-secondary.example.com',
|
||||
};
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
console.log('DNS server started with custom primary nameserver');
|
||||
|
||||
// SOA records will now use 'ns-primary.example.com' instead of 'ns1.example.com'
|
||||
}
|
||||
|
||||
// Example: DNSSEC with multiple records (proper RRset signing)
|
||||
async function exampleDnssecMultipleRecords() {
|
||||
const dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: 'your-https-key',
|
||||
httpsCert: 'your-https-cert',
|
||||
httpsPort: 8443,
|
||||
udpPort: 8053,
|
||||
dnssecZone: 'secure.example.com',
|
||||
});
|
||||
|
||||
// Register multiple A records for round-robin
|
||||
const ips = ['192.168.1.10', '192.168.1.11', '192.168.1.12'];
|
||||
for (const ip of ips) {
|
||||
dnsServer.registerHandler('www.secure.example.com', ['A'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
ttl: 300,
|
||||
data: ip,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await dnsServer.start();
|
||||
console.log('DNS server started with DNSSEC and multiple A records');
|
||||
|
||||
// When queried with DNSSEC enabled, all 3 A records will be signed together
|
||||
// as a single RRset with one RRSIG record (not 3 separate RRSIGs)
|
||||
}
|
||||
|
||||
// Example: Multiple TXT records for various purposes
|
||||
async function exampleMultipleTxtRecords() {
|
||||
const dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: 'your-https-key',
|
||||
httpsCert: 'your-https-cert',
|
||||
httpsPort: 8443,
|
||||
udpPort: 8053,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// SPF record
|
||||
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'TXT',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: ['v=spf1 include:_spf.google.com ~all'],
|
||||
};
|
||||
});
|
||||
|
||||
// DKIM record
|
||||
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'TXT',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: ['v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4...'],
|
||||
};
|
||||
});
|
||||
|
||||
// Domain verification
|
||||
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'TXT',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: ['google-site-verification=1234567890abcdef'],
|
||||
};
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
console.log('DNS server started with multiple TXT records');
|
||||
|
||||
// All TXT records will be returned when queried
|
||||
}
|
||||
|
||||
// Export examples for reference
|
||||
export { exampleCustomNameserver, exampleDnssecMultipleRecords, exampleMultipleTxtRecords };
|
373
test/test.dnssec.rrset.ts
Normal file
373
test/test.dnssec.rrset.ts
Normal file
@ -0,0 +1,373 @@
|
||||
import * as plugins from '../ts_server/plugins.js';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
import * as dgram from 'dgram';
|
||||
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
let dnsServer: smartdns.DnsServer;
|
||||
|
||||
// Port management for tests
|
||||
let nextHttpsPort = 8500;
|
||||
let nextUdpPort = 8501;
|
||||
|
||||
function getUniqueHttpsPort() {
|
||||
return nextHttpsPort++;
|
||||
}
|
||||
|
||||
function getUniqueUdpPort() {
|
||||
return nextUdpPort++;
|
||||
}
|
||||
|
||||
// Cleanup function for servers
|
||||
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (e) {
|
||||
console.log('Handled error when stopping server:', e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('DNSSEC should sign entire RRset together, not individual records', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// Register multiple NS record handlers
|
||||
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'NS',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: 'ns1.example.com',
|
||||
};
|
||||
});
|
||||
|
||||
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'NS',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: 'ns2.example.com',
|
||||
};
|
||||
});
|
||||
|
||||
dnsServer.registerHandler('example.com', ['NS'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'NS',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: 'ns3.example.com',
|
||||
};
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
// Create query with DNSSEC requested
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'example.com',
|
||||
type: 'NS',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
additionals: [
|
||||
{
|
||||
name: '.',
|
||||
type: 'OPT',
|
||||
ttl: 0,
|
||||
flags: 0x8000, // DO bit set for DNSSEC
|
||||
data: Buffer.alloc(0),
|
||||
} as any,
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
// Count NS and RRSIG records
|
||||
const nsAnswers = dnsResponse.answers.filter(a => a.type === 'NS');
|
||||
const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG');
|
||||
|
||||
console.log('NS records returned:', nsAnswers.length);
|
||||
console.log('RRSIG records returned:', rrsigAnswers.length);
|
||||
|
||||
// Should have 3 NS records and only 1 RRSIG for the entire RRset
|
||||
expect(nsAnswers.length).toEqual(3);
|
||||
expect(rrsigAnswers.length).toEqual(1);
|
||||
|
||||
// Verify RRSIG covers NS type
|
||||
const rrsigData = (rrsigAnswers[0] as any).data;
|
||||
expect(rrsigData.typeCovered).toEqual('NS');
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('SOA records should be properly serialized and returned', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
// Query for a non-existent subdomain to trigger SOA response
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 2,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
// Should have SOA record in response
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('SOA record:', soaData);
|
||||
|
||||
expect(soaData.mname).toEqual('ns1.example.com');
|
||||
expect(soaData.rname).toEqual('hostmaster.example.com');
|
||||
expect(typeof soaData.serial).toEqual('number');
|
||||
expect(soaData.refresh).toEqual(3600);
|
||||
expect(soaData.retry).toEqual(600);
|
||||
expect(soaData.expire).toEqual(604800);
|
||||
expect(soaData.minimum).toEqual(86400);
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Primary nameserver should be configurable', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
primaryNameserver: 'custom-ns.example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
// Query for SOA record
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 3,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'example.com',
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
// Should have SOA record with custom nameserver
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('SOA mname:', soaData.mname);
|
||||
|
||||
// Should use the custom primary nameserver
|
||||
expect(soaData.mname).toEqual('custom-ns.example.com');
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Multiple A records should have single RRSIG when DNSSEC is enabled', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// Register multiple A records for round-robin
|
||||
const ips = ['10.0.0.1', '10.0.0.2', '10.0.0.3'];
|
||||
for (const ip of ips) {
|
||||
dnsServer.registerHandler('www.example.com', ['A'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
ttl: 300,
|
||||
data: ip,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 4,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'www.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
additionals: [
|
||||
{
|
||||
name: '.',
|
||||
type: 'OPT',
|
||||
ttl: 0,
|
||||
flags: 0x8000, // DO bit set for DNSSEC
|
||||
data: Buffer.alloc(0),
|
||||
} as any,
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
const aAnswers = dnsResponse.answers.filter(a => a.type === 'A');
|
||||
const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG');
|
||||
|
||||
console.log('A records:', aAnswers.length);
|
||||
console.log('RRSIG records:', rrsigAnswers.length);
|
||||
|
||||
// Should have 3 A records and only 1 RRSIG
|
||||
expect(aAnswers.length).toEqual(3);
|
||||
expect(rrsigAnswers.length).toEqual(1);
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
export default tap.start();
|
228
test/test.fixes.simple.ts
Normal file
228
test/test.fixes.simple.ts
Normal file
@ -0,0 +1,228 @@
|
||||
import * as plugins from '../ts_server/plugins.js';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
import * as dgram from 'dgram';
|
||||
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
let dnsServer: smartdns.DnsServer;
|
||||
|
||||
// Port management for tests
|
||||
let nextHttpsPort = 8600;
|
||||
let nextUdpPort = 8601;
|
||||
|
||||
function getUniqueHttpsPort() {
|
||||
return nextHttpsPort++;
|
||||
}
|
||||
|
||||
function getUniqueUdpPort() {
|
||||
return nextUdpPort++;
|
||||
}
|
||||
|
||||
// Cleanup function for servers
|
||||
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (e) {
|
||||
console.log('Handled error when stopping server:', e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('SOA records should be returned for non-existent domains', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
console.log('✅ SOA response received');
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('✅ SOA mname:', soaData.mname);
|
||||
console.log('✅ SOA rname:', soaData.rname);
|
||||
|
||||
expect(soaData.mname).toEqual('ns1.example.com');
|
||||
expect(soaData.rname).toEqual('hostmaster.example.com');
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Primary nameserver should be configurable', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
primaryNameserver: 'custom-ns.example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 2,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('✅ Custom primary nameserver:', soaData.mname);
|
||||
|
||||
expect(soaData.mname).toEqual('custom-ns.example.com');
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Default primary nameserver with FQDN', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
primaryNameserver: 'ns.example.com.', // FQDN with trailing dot
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 3,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
client.on('message', (msg) => {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('✅ FQDN primary nameserver:', soaData.mname);
|
||||
|
||||
expect(soaData.mname).toEqual('ns.example.com.');
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -396,9 +396,9 @@ tap.test('should handle DNSSEC correctly with multiple records', async () => {
|
||||
console.log('NS records:', nsAnswers.length);
|
||||
console.log('RRSIG records:', rrsigAnswers.length);
|
||||
|
||||
// With DNSSEC, each NS record should have an associated RRSIG
|
||||
// With DNSSEC RRset signing, all NS records share ONE RRSIG (entire RRset signed together)
|
||||
expect(nsAnswers.length).toEqual(2);
|
||||
expect(rrsigAnswers.length).toEqual(2);
|
||||
expect(rrsigAnswers.length).toEqual(1);
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
|
269
test/test.soa.debug.ts
Normal file
269
test/test.soa.debug.ts
Normal file
@ -0,0 +1,269 @@
|
||||
import * as plugins from '../ts_server/plugins.js';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
import * as dgram from 'dgram';
|
||||
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
let dnsServer: smartdns.DnsServer;
|
||||
|
||||
// Port management for tests
|
||||
let nextHttpsPort = 8700;
|
||||
let nextUdpPort = 8701;
|
||||
|
||||
function getUniqueHttpsPort() {
|
||||
return nextHttpsPort++;
|
||||
}
|
||||
|
||||
function getUniqueUdpPort() {
|
||||
return nextUdpPort++;
|
||||
}
|
||||
|
||||
// Cleanup function for servers
|
||||
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (e) {
|
||||
console.log('Handled error when stopping server:', e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('Direct SOA query should work without timeout', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// Register a SOA handler directly
|
||||
dnsServer.registerHandler('example.com', ['SOA'], (question) => {
|
||||
console.log('Direct SOA handler called for:', question.name);
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: {
|
||||
mname: 'ns1.example.com',
|
||||
rname: 'hostmaster.example.com',
|
||||
serial: 2024010101,
|
||||
refresh: 3600,
|
||||
retry: 600,
|
||||
expire: 604800,
|
||||
minimum: 86400,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'example.com',
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
console.log('Sending SOA query for example.com');
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out after 5 seconds'));
|
||||
}, 5000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
} catch (e) {
|
||||
reject(new Error(`Failed to decode response: ${e.message}`));
|
||||
}
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const dnsResponse = await responsePromise;
|
||||
console.log('SOA response received:', dnsResponse.answers.length, 'answers');
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('SOA data:', soaData);
|
||||
|
||||
expect(soaData.mname).toEqual('ns1.example.com');
|
||||
expect(soaData.serial).toEqual(2024010101);
|
||||
} catch (error) {
|
||||
console.error('SOA query failed:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('SOA query with DNSSEC should work', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 2,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
additionals: [
|
||||
{
|
||||
name: '.',
|
||||
type: 'OPT',
|
||||
ttl: 0,
|
||||
flags: 0x8000, // DO bit set for DNSSEC
|
||||
data: Buffer.alloc(0),
|
||||
} as any,
|
||||
],
|
||||
});
|
||||
|
||||
console.log('Sending query for nonexistent domain with DNSSEC');
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out after 5 seconds'));
|
||||
}, 5000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
} catch (e) {
|
||||
reject(new Error(`Failed to decode response: ${e.message}`));
|
||||
}
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const dnsResponse = await responsePromise;
|
||||
console.log('Response received with', dnsResponse.answers.length, 'answers');
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
console.log('SOA records found:', soaAnswers.length);
|
||||
|
||||
if (soaAnswers.length > 0) {
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('SOA data:', soaData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SOA query with DNSSEC failed:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Test raw SOA serialization', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: getUniqueUdpPort(),
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// Test the serializeRData method directly
|
||||
const soaData = {
|
||||
mname: 'ns1.example.com',
|
||||
rname: 'hostmaster.example.com',
|
||||
serial: 2024010101,
|
||||
refresh: 3600,
|
||||
retry: 600,
|
||||
expire: 604800,
|
||||
minimum: 86400,
|
||||
};
|
||||
|
||||
try {
|
||||
// @ts-ignore - accessing private method for testing
|
||||
const serialized = dnsServer.serializeRData('SOA', soaData);
|
||||
console.log('SOA serialized successfully, buffer length:', serialized.length);
|
||||
expect(serialized.length).toBeGreaterThan(0);
|
||||
|
||||
// The buffer should contain the serialized domain names + 5 * 4 bytes for the numbers
|
||||
// Domain names have variable length, but should be at least 20 bytes total
|
||||
expect(serialized.length).toBeGreaterThan(20);
|
||||
} catch (error) {
|
||||
console.error('SOA serialization failed:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
271
test/test.soa.final.ts
Normal file
271
test/test.soa.final.ts
Normal file
@ -0,0 +1,271 @@
|
||||
import * as plugins from '../ts_server/plugins.js';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
import * as dgram from 'dgram';
|
||||
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
let dnsServer: smartdns.DnsServer;
|
||||
|
||||
// Port management for tests
|
||||
let nextHttpsPort = 8900;
|
||||
let nextUdpPort = 8901;
|
||||
|
||||
function getUniqueHttpsPort() {
|
||||
return nextHttpsPort++;
|
||||
}
|
||||
|
||||
function getUniqueUdpPort() {
|
||||
return nextUdpPort++;
|
||||
}
|
||||
|
||||
// Cleanup function for servers
|
||||
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (e) {
|
||||
console.log('Handled error when stopping server:', e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('SOA records work for all scenarios', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
primaryNameserver: 'ns.example.com',
|
||||
});
|
||||
|
||||
// Register SOA handler for the zone
|
||||
dnsServer.registerHandler('example.com', ['SOA'], (question) => {
|
||||
console.log('SOA handler called for:', question.name);
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: {
|
||||
mname: 'ns.example.com',
|
||||
rname: 'admin.example.com',
|
||||
serial: 2024010101,
|
||||
refresh: 3600,
|
||||
retry: 600,
|
||||
expire: 604800,
|
||||
minimum: 86400,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Register some other records
|
||||
dnsServer.registerHandler('example.com', ['A'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
ttl: 300,
|
||||
data: '192.168.1.1',
|
||||
};
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
// Test 1: Direct SOA query
|
||||
console.log('\n--- Test 1: Direct SOA query ---');
|
||||
const soaQuery = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'example.com',
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let response = await new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out'));
|
||||
}, 2000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.removeAllListeners();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(soaQuery, udpPort, 'localhost');
|
||||
});
|
||||
|
||||
console.log('Direct SOA query response:', response.answers.length, 'answers');
|
||||
expect(response.answers.length).toEqual(1);
|
||||
expect(response.answers[0].type).toEqual('SOA');
|
||||
|
||||
// Test 2: Non-existent domain query (should get SOA in authority)
|
||||
console.log('\n--- Test 2: Non-existent domain query ---');
|
||||
const nxQuery = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 2,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
response = await new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out'));
|
||||
}, 2000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.removeAllListeners();
|
||||
});
|
||||
|
||||
client.send(nxQuery, udpPort, 'localhost');
|
||||
});
|
||||
|
||||
console.log('Non-existent query response:', response.answers.length, 'answers');
|
||||
const soaAnswers = response.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
// Test 3: SOA with DNSSEC
|
||||
console.log('\n--- Test 3: SOA query with DNSSEC ---');
|
||||
const dnssecQuery = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 3,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'example.com',
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
additionals: [
|
||||
{
|
||||
name: '.',
|
||||
type: 'OPT',
|
||||
ttl: 0,
|
||||
flags: 0x8000, // DO bit
|
||||
data: Buffer.alloc(0),
|
||||
} as any,
|
||||
],
|
||||
});
|
||||
|
||||
response = await new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out'));
|
||||
}, 2000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
client.removeAllListeners();
|
||||
});
|
||||
|
||||
client.send(dnssecQuery, udpPort, 'localhost');
|
||||
});
|
||||
|
||||
console.log('DNSSEC SOA query response:', response.answers.length, 'answers');
|
||||
console.log('Answer types:', response.answers.map(a => a.type));
|
||||
expect(response.answers.length).toEqual(2); // SOA + RRSIG
|
||||
expect(response.answers.some(a => a.type === 'SOA')).toEqual(true);
|
||||
expect(response.answers.some(a => a.type === 'RRSIG')).toEqual(true);
|
||||
|
||||
client.close();
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Configurable primary nameserver works correctly', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'test.com',
|
||||
primaryNameserver: 'master.test.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.test.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out'));
|
||||
}, 2000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost');
|
||||
});
|
||||
|
||||
const soaAnswers = response.answers.filter(a => a.type === 'SOA');
|
||||
console.log('✅ Configured primary nameserver:', (soaAnswers[0] as any).data.mname);
|
||||
expect((soaAnswers[0] as any).data.mname).toEqual('master.test.com');
|
||||
|
||||
client.close();
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
export default tap.start();
|
201
test/test.soa.simple.ts
Normal file
201
test/test.soa.simple.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import * as plugins from '../ts_server/plugins.js';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
import * as dgram from 'dgram';
|
||||
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
let dnsServer: smartdns.DnsServer;
|
||||
|
||||
// Port management for tests
|
||||
let nextHttpsPort = 8800;
|
||||
let nextUdpPort = 8801;
|
||||
|
||||
function getUniqueHttpsPort() {
|
||||
return nextHttpsPort++;
|
||||
}
|
||||
|
||||
function getUniqueUdpPort() {
|
||||
return nextUdpPort++;
|
||||
}
|
||||
|
||||
// Cleanup function for servers
|
||||
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (e) {
|
||||
console.log('Handled error when stopping server:', e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('Simple SOA query without DNSSEC', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
// Query for a non-existent domain WITHOUT DNSSEC
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out'));
|
||||
}, 2000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
console.log('✅ SOA response without DNSSEC received');
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
const soaData = (soaAnswers[0] as any).data;
|
||||
console.log('✅ SOA data:', soaData.mname);
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Direct SOA query without DNSSEC', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = getUniqueUdpPort();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: getUniqueHttpsPort(),
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// Register direct SOA handler
|
||||
dnsServer.registerHandler('example.com', ['SOA'], (question) => {
|
||||
return {
|
||||
name: question.name,
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
ttl: 3600,
|
||||
data: {
|
||||
mname: 'ns1.example.com',
|
||||
rname: 'hostmaster.example.com',
|
||||
serial: 2024010101,
|
||||
refresh: 3600,
|
||||
retry: 600,
|
||||
expire: 604800,
|
||||
minimum: 86400,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 2,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'example.com',
|
||||
type: 'SOA',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
reject(new Error('Query timed out'));
|
||||
}, 2000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dnsResponse = await responsePromise;
|
||||
console.log('✅ Direct SOA query succeeded');
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
expect(soaAnswers.length).toEqual(1);
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
export default tap.start();
|
224
test/test.soa.timeout.ts
Normal file
224
test/test.soa.timeout.ts
Normal file
@ -0,0 +1,224 @@
|
||||
import * as plugins from '../ts_server/plugins.js';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
|
||||
import * as dnsPacket from 'dns-packet';
|
||||
import * as dgram from 'dgram';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import * as smartdns from '../ts_server/index.js';
|
||||
|
||||
let dnsServer: smartdns.DnsServer;
|
||||
|
||||
// Port management for tests
|
||||
const testPort = 8753;
|
||||
|
||||
// Cleanup function for servers
|
||||
async function stopServer(server: smartdns.DnsServer | null | undefined) {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (e) {
|
||||
console.log('Handled error when stopping server:', e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('Test SOA timeout with real dig command', async (tools) => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: 8752,
|
||||
udpPort: testPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
console.log(`DNS server started on port ${testPort}`);
|
||||
|
||||
// Test with dig command
|
||||
try {
|
||||
console.log('Testing SOA query with dig...');
|
||||
const result = execSync(`dig @localhost -p ${testPort} example.com SOA +timeout=3`, { encoding: 'utf8' });
|
||||
console.log('Dig SOA query result:', result);
|
||||
|
||||
// Check if we got an answer section
|
||||
expect(result).toInclude('ANSWER SECTION');
|
||||
expect(result).toInclude('SOA');
|
||||
} catch (error) {
|
||||
console.error('Dig command failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Test nonexistent domain SOA
|
||||
try {
|
||||
console.log('Testing nonexistent domain SOA query with dig...');
|
||||
const result = execSync(`dig @localhost -p ${testPort} nonexistent.example.com A +timeout=3`, { encoding: 'utf8' });
|
||||
console.log('Dig nonexistent query result:', result);
|
||||
|
||||
// Should get AUTHORITY section with SOA
|
||||
expect(result).toInclude('AUTHORITY SECTION');
|
||||
} catch (error) {
|
||||
console.error('Dig nonexistent query failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Test SOA with DNSSEC timing', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
const udpPort = 8754;
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: 8755,
|
||||
udpPort: udpPort,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
const client = dgram.createSocket('udp4');
|
||||
|
||||
// Test with DNSSEC enabled
|
||||
const query = dnsPacket.encode({
|
||||
type: 'query',
|
||||
id: 1,
|
||||
flags: dnsPacket.RECURSION_DESIRED,
|
||||
questions: [
|
||||
{
|
||||
name: 'nonexistent.example.com',
|
||||
type: 'A',
|
||||
class: 'IN',
|
||||
},
|
||||
],
|
||||
additionals: [
|
||||
{
|
||||
name: '.',
|
||||
type: 'OPT',
|
||||
ttl: 0,
|
||||
flags: 0x8000, // DO bit set for DNSSEC
|
||||
data: Buffer.alloc(0),
|
||||
} as any,
|
||||
],
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
console.log('Sending DNSSEC query for nonexistent domain...');
|
||||
|
||||
const responsePromise = new Promise<dnsPacket.Packet>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.close();
|
||||
const elapsed = Date.now() - startTime;
|
||||
reject(new Error(`Query timed out after ${elapsed}ms`));
|
||||
}, 3000);
|
||||
|
||||
client.on('message', (msg) => {
|
||||
clearTimeout(timeout);
|
||||
const elapsed = Date.now() - startTime;
|
||||
console.log(`Response received in ${elapsed}ms`);
|
||||
|
||||
try {
|
||||
const dnsResponse = dnsPacket.decode(msg);
|
||||
resolve(dnsResponse);
|
||||
} catch (e) {
|
||||
reject(new Error(`Failed to decode response: ${e.message}`));
|
||||
}
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
const elapsed = Date.now() - startTime;
|
||||
console.error(`Error after ${elapsed}ms:`, err);
|
||||
reject(err);
|
||||
client.close();
|
||||
});
|
||||
|
||||
client.send(query, udpPort, 'localhost', (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const dnsResponse = await responsePromise;
|
||||
console.log('Response details:');
|
||||
console.log('- Answers:', dnsResponse.answers.length);
|
||||
console.log('- Answer types:', dnsResponse.answers.map(a => a.type));
|
||||
|
||||
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
||||
const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG');
|
||||
|
||||
console.log('- SOA records:', soaAnswers.length);
|
||||
console.log('- RRSIG records:', rrsigAnswers.length);
|
||||
|
||||
// With the fix, SOA should have its RRSIG
|
||||
if (soaAnswers.length > 0) {
|
||||
expect(rrsigAnswers.length).toBeGreaterThan(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('DNSSEC SOA query failed:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await stopServer(dnsServer);
|
||||
dnsServer = null;
|
||||
});
|
||||
|
||||
tap.test('Check DNSSEC signing performance for SOA', async () => {
|
||||
const httpsData = await tapNodeTools.createHttpsCert();
|
||||
|
||||
dnsServer = new smartdns.DnsServer({
|
||||
httpsKey: httpsData.key,
|
||||
httpsCert: httpsData.cert,
|
||||
httpsPort: 8756,
|
||||
udpPort: 8757,
|
||||
dnssecZone: 'example.com',
|
||||
});
|
||||
|
||||
// Time SOA serialization
|
||||
const soaData = {
|
||||
mname: 'ns1.example.com',
|
||||
rname: 'hostmaster.example.com',
|
||||
serial: 2024010101,
|
||||
refresh: 3600,
|
||||
retry: 600,
|
||||
expire: 604800,
|
||||
minimum: 86400,
|
||||
};
|
||||
|
||||
console.log('Testing SOA serialization performance...');
|
||||
const serializeStart = Date.now();
|
||||
|
||||
try {
|
||||
// @ts-ignore - accessing private method for testing
|
||||
const serialized = dnsServer.serializeRData('SOA', soaData);
|
||||
const serializeTime = Date.now() - serializeStart;
|
||||
console.log(`SOA serialization took ${serializeTime}ms`);
|
||||
|
||||
// Test DNSSEC signing
|
||||
const signStart = Date.now();
|
||||
// @ts-ignore - accessing private property
|
||||
const signature = dnsServer.dnsSec.signData(serialized);
|
||||
const signTime = Date.now() - signStart;
|
||||
console.log(`DNSSEC signing took ${signTime}ms`);
|
||||
|
||||
expect(serializeTime).toBeLessThan(100); // Should be fast
|
||||
expect(signTime).toBeLessThan(500); // Signing can take longer but shouldn't timeout
|
||||
} catch (error) {
|
||||
console.error('Performance test failed:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartdns',
|
||||
version: '7.4.2',
|
||||
version: '7.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.'
|
||||
}
|
||||
|
@ -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;
|
||||
// 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
|
||||
},
|
||||
};
|
||||
|
||||
@ -760,9 +794,27 @@ export class DnsServer {
|
||||
// NS records contain domain names
|
||||
return this.nameToBuffer(data);
|
||||
case 'SOA':
|
||||
// Implement SOA record serialization if needed
|
||||
// For now, return an empty buffer or handle as needed
|
||||
return Buffer.alloc(0);
|
||||
// Implement SOA record serialization according to RFC 1035
|
||||
const mname = this.nameToBuffer(data.mname);
|
||||
const rname = this.nameToBuffer(data.rname);
|
||||
const serial = Buffer.alloc(4);
|
||||
serial.writeUInt32BE(data.serial, 0);
|
||||
const refresh = Buffer.alloc(4);
|
||||
refresh.writeUInt32BE(data.refresh, 0);
|
||||
const retry = Buffer.alloc(4);
|
||||
retry.writeUInt32BE(data.retry, 0);
|
||||
const expire = Buffer.alloc(4);
|
||||
expire.writeUInt32BE(data.expire, 0);
|
||||
const minimum = Buffer.alloc(4);
|
||||
minimum.writeUInt32BE(data.minimum, 0);
|
||||
|
||||
return Buffer.concat([mname, rname, serial, refresh, retry, expire, minimum]);
|
||||
case 'MX':
|
||||
// MX records contain preference (16-bit) and exchange (domain name)
|
||||
const preference = Buffer.alloc(2);
|
||||
preference.writeUInt16BE(data.preference, 0);
|
||||
const exchange = this.nameToBuffer(data.exchange);
|
||||
return Buffer.concat([preference, exchange]);
|
||||
// Add cases for other record types as needed
|
||||
default:
|
||||
throw new Error(`Serialization for record type ${type} is not implemented.`);
|
||||
|
Loading…
x
Reference in New Issue
Block a user