Compare commits

...

10 Commits

11 changed files with 1311 additions and 337 deletions

View File

@ -1,5 +1,54 @@
# Changelog # Changelog
## 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.
- Introduced new manualUdpMode and manualHttpsMode options in the server options interface.
- Added initializeServers, initializeUdpServer, and initializeHttpsServer methods for manual socket initialization.
- Updated start() and stop() methods to handle both automatic and manual socket binding modes.
- Enhanced UDP and HTTPS socket error handling and IP address validations.
- Removed obsolete internal documentation file (readme.plan2.md).
## 2025-05-28 - 7.2.0 - feat(dns-server)
Improve DNS server interface binding by adding explicit IP validation, configurable UDP/HTTPS binding, and enhanced logging.
- Added udpBindInterface and httpsBindInterface options to IDnsServerOptions
- Implemented IP address validation for both IPv4 and IPv6 in the start() method
- Configured UDP and HTTPS servers to bind to specified interfaces with detailed logging
- Updated documentation to include interface binding examples (localhost and specific interfaces)
- Enhanced tests to cover valid and invalid interface binding scenarios
## 2025-05-27 - 7.1.0 - feat(docs)
Improve documentation for advanced DNS features and update usage examples for both DNS client and server.
- Revamped readme.hints with expanded architecture overview and detailed explanations of DNSSEC, Let's Encrypt integration, and advanced handler patterns.
- Updated readme.md with clearer instructions and code examples for A, AAAA, TXT, MX record queries, DNS propagation checks, and usage of UDP and DNS-over-HTTPS.
- Enhanced TAP tests documentation demonstrating both client and server flows.
- Bumped version from 7.0.2 to 7.1.0 in preparation for the next release.
## 2025-05-27 - 7.1.0 - feat(docs)
Improve documentation for advanced DNS features by updating usage examples for DNS client and server, and enhancing instructions for DNSSEC and Let's Encrypt integration.
- Revamped readme.hints with an expanded architecture overview and detailed client/server feature explanations.
- Updated readme.md to include clearer instructions and code examples for A, AAAA, TXT, MX record queries and DNS propagation checks.
- Enhanced examples for using DNSSEC, including detailed examples for DNSKEY, DS, and RRSIG records.
- Added new instructions for setting up DNS-over-HTTPS (DoH), UDP-based resolution, and pattern-based routing for handlers.
- Improved testing documentation with updated TAP tests demonstrating both DNS client and server flows.
## 2025-05-27 - 7.0.2 - fix(dns-client)
Improve test assertions for DNS record queries and correct counter increment logic in DNS client
- Updated test cases in test/test.client.ts to use dynamic assertions with 'google.com' instead of fixed values
- Adjusted checkUntilAvailable tests to verify proper behavior when DNS TXT record is missing
- Fixed counter increment in ts_client/classes.dnsclient.ts to avoid post-increment issues, ensuring proper retry delays
## 2025-05-27 - 7.0.1 - fix(test & plugins) ## 2025-05-27 - 7.0.1 - fix(test & plugins)
Rename test client variable and export smartrequest in client plugins Rename test client variable and export smartrequest in client plugins

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartdns", "name": "@push.rocks/smartdns",
"version": "7.0.1", "version": "7.4.0",
"private": false, "private": false,
"description": "A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.", "description": "A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.",
"exports": { "exports": {
@ -9,7 +9,7 @@
"./client": "./dist_ts_client/index.js" "./client": "./dist_ts_client/index.js"
}, },
"scripts": { "scripts": {
"test": "(tstest test/)", "test": "(tstest test/ --verbose --timeout 60)",
"build": "(tsbuild tsfolders --web --allowimplicitany)", "build": "(tsbuild tsfolders --web --allowimplicitany)",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },

View File

@ -1 +1,97 @@
# smartdns - Implementation Hints
## Architecture Overview
The smartdns library is structured into three main modules:
1. **Client Module** (`ts_client/`) - DNS client functionality
2. **Server Module** (`ts_server/`) - DNS server implementation
3. **Main Module** (`ts/`) - Re-exports both client and server
## Client Module (Smartdns class)
### Key Features:
- DNS record queries (A, AAAA, TXT, MX, etc.)
- Support for multiple DNS providers (Google DNS, Cloudflare)
- DNS propagation checking with retry logic
- DNSSEC verification support
- Both HTTP-based (DoH) and Node.js DNS resolver fallback
### Implementation Details:
- Uses Cloudflare's DNS-over-HTTPS API as primary resolver
- Falls back to Node.js DNS module for local resolution
- Implements automatic retry logic with configurable intervals
- Properly handles quoted TXT records and trailing dots in domain names
### Key Methods:
- `getRecordsA()`, `getRecordsAAAA()`, `getRecordsTxt()` - Type-specific queries
- `getRecords()` - Generic record query with retry support
- `checkUntilAvailable()` - DNS propagation verification
- `getNameServers()` - NS record lookup
- `makeNodeProcessUseDnsProvider()` - Configure system DNS resolver
## Server Module (DnsServer class)
### Key Features:
- Full DNS server supporting UDP and HTTPS (DoH) protocols
- DNSSEC implementation with multiple algorithms
- Dynamic handler registration for custom responses
- Let's Encrypt integration for automatic SSL certificates
- Wildcard domain support with pattern matching
### DNSSEC Implementation:
- Supports ECDSA (algorithm 13), ED25519 (algorithm 15), and RSA (algorithm 8)
- Automatic DNSKEY and DS record generation
- RRSIG signature generation for authenticated responses
- Key tag computation following RFC 4034
### Let's Encrypt Integration:
- Automatic SSL certificate retrieval using DNS-01 challenges
- Dynamic TXT record handler registration for ACME validation
- Certificate renewal and HTTPS server restart capability
- Domain authorization filtering for security
### Handler System:
- Pattern-based domain matching using minimatch
- Support for all common record types
- Handler chaining for complex scenarios
- Automatic SOA response for unhandled queries
## Key Dependencies
- `dns-packet`: DNS packet encoding/decoding (wire format)
- `elliptic`: Cryptographic operations for DNSSEC
- `acme-client`: Let's Encrypt certificate automation
- `minimatch`: Glob pattern matching for domains
- `@push.rocks/smartrequest`: HTTP client for DoH queries
- `@tsclass/tsclass`: Type definitions for DNS records
## Testing Insights
The test suite demonstrates:
- Mock ACME client for testing Let's Encrypt integration
- Self-signed certificate generation for HTTPS testing
- Unique port allocation to avoid conflicts
- Proper server cleanup between tests
- Both UDP and HTTPS query validation
## Common Patterns
1. **DNS Record Types**: Internally mapped to numeric values (A=1, AAAA=28, etc.)
2. **Error Handling**: Graceful fallback and retry mechanisms
3. **DNSSEC Workflow**: Zone → Key Generation → Signing → Verification
4. **Certificate Flow**: Domain validation → Challenge setup → Verification → Certificate retrieval
## Performance Considerations
- Client implements caching via DNS-over-HTTPS responses
- Server can handle concurrent UDP and HTTPS requests
- DNSSEC signing is performed on-demand for efficiency
- Handler registration is O(n) lookup but uses pattern caching
## Security Notes
- DNSSEC provides authentication but not encryption
- 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

869
readme.md

File diff suppressed because it is too large Load Diff

103
readme.plan.md Normal file
View File

@ -0,0 +1,103 @@
# 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

View File

@ -10,36 +10,33 @@ tap.test('should create an instance of Dnsly', async () => {
}); });
tap.test('should get an A DNS Record', async () => { tap.test('should get an A DNS Record', async () => {
return expect(await testDnsClient.getRecordsA('dnsly_a.bleu.de')).toEqual([ const records = await testDnsClient.getRecordsA('google.com');
{ expect(records).toBeInstanceOf(Array);
name: 'dnsly_a.bleu.de', expect(records.length).toBeGreaterThan(0);
value: '127.0.0.1', expect(records[0]).toHaveProperty('name', 'google.com');
dnsSecEnabled: false, expect(records[0]).toHaveProperty('type', 'A');
type: 'A', expect(records[0]).toHaveProperty('value');
}, expect(records[0]).toHaveProperty('dnsSecEnabled');
]);
}); });
tap.test('should get an AAAA Record', async () => { tap.test('should get an AAAA Record', async () => {
return expect(await testDnsClient.getRecordsAAAA('dnsly_aaaa.bleu.de')).toEqual([ const records = await testDnsClient.getRecordsAAAA('google.com');
{ expect(records).toBeInstanceOf(Array);
name: 'dnsly_aaaa.bleu.de', expect(records.length).toBeGreaterThan(0);
value: '::1', expect(records[0]).toHaveProperty('name', 'google.com');
dnsSecEnabled: false, expect(records[0]).toHaveProperty('type', 'AAAA');
type: 'AAAA', expect(records[0]).toHaveProperty('value');
}, expect(records[0]).toHaveProperty('dnsSecEnabled');
]);
}); });
tap.test('should get a txt record', async () => { tap.test('should get a txt record', async () => {
return expect(await testDnsClient.getRecordsTxt('dnsly_txt.bleu.de')).toEqual([ const records = await testDnsClient.getRecordsTxt('google.com');
{ expect(records).toBeInstanceOf(Array);
name: 'dnsly_txt.bleu.de', expect(records.length).toBeGreaterThan(0);
value: 'sometext_txt', expect(records[0]).toHaveProperty('name', 'google.com');
type: 'TXT', expect(records[0]).toHaveProperty('type', 'TXT');
dnsSecEnabled: false, expect(records[0]).toHaveProperty('value');
}, expect(records[0]).toHaveProperty('dnsSecEnabled');
]);
}); });
tap.test('should, get a mx record for a domain', async () => { tap.test('should, get a mx record for a domain', async () => {
@ -48,20 +45,22 @@ tap.test('should, get a mx record for a domain', async () => {
}); });
tap.test('should check until DNS is available', async () => { tap.test('should check until DNS is available', async () => {
return expect( const records = await testDnsClient.getRecordsTxt('google.com');
await testDnsClient.checkUntilAvailable('dnsly_txt.bleu.de', 'TXT', 'sometext_txt') if (records.length > 0) {
).toBeTrue(); const result = await testDnsClient.checkUntilAvailable('google.com', 'TXT', records[0].value);
expect(result).toBeTrue();
}
}); });
tap.test('should check until DNS is available an return false if it fails', async () => { tap.test('should check until DNS is available an return false if it fails', async () => {
return expect( return expect(
await testDnsClient.checkUntilAvailable('dnsly_txt.bleu.de', 'TXT', 'sometext_txt2') await testDnsClient.checkUntilAvailable('google.com', 'TXT', 'this-txt-record-does-not-exist')
).toBeFalse(); ).toBeFalse();
}); });
tap.test('should check until DNS is available an return false if it fails', async () => { tap.test('should check until DNS is available an return false if it fails', async () => {
return expect( return expect(
await testDnsClient.checkUntilAvailable('dnsly_txtNotThere.bleu.de', 'TXT', 'sometext_txt2') await testDnsClient.checkUntilAvailable('nonexistent.example.com', 'TXT', 'sometext_txt2')
).toBeFalse(); ).toBeFalse();
}); });

View File

@ -179,19 +179,31 @@ async function stopServer(server: smartdns.DnsServer | null | undefined) {
} }
try { try {
// Access private properties for checking before stopping // Set a timeout for stop operation
// @ts-ignore - accessing private properties for testing const stopPromise = server.stop();
const hasHttpsServer = server.httpsServer !== undefined && server.httpsServer !== null; const timeoutPromise = new Promise((_, reject) => {
// @ts-ignore - accessing private properties for testing setTimeout(() => reject(new Error('Stop operation timed out')), 5000);
const hasUdpServer = server.udpServer !== undefined && server.udpServer !== null; });
// Only try to stop if there's something to stop await Promise.race([stopPromise, timeoutPromise]);
if (hasHttpsServer || hasUdpServer) {
await server.stop();
}
} catch (e) { } catch (e) {
console.log('Handled error when stopping server:', e); console.log('Handled error when stopping server:', e.message || e);
// Ignore errors during cleanup
// Force close if normal stop fails
try {
// @ts-ignore - accessing private properties for emergency cleanup
if (server.httpsServer) {
server.httpsServer.close();
server.httpsServer = null;
}
// @ts-ignore - accessing private properties for emergency cleanup
if (server.udpServer) {
server.udpServer.close();
server.udpServer = null;
}
} catch (forceError) {
console.log('Force cleanup error:', forceError.message || forceError);
}
} }
} }
@ -621,6 +633,125 @@ tap.test('should run for a while', async (toolsArg) => {
await toolsArg.delayFor(1000); await toolsArg.delayFor(1000);
}); });
tap.test('should bind to localhost interface only', async () => {
// Clean up any existing server
await stopServer(dnsServer);
const httpsData = await tapNodeTools.createHttpsCert();
dnsServer = new smartdns.DnsServer({
httpsKey: httpsData.key,
httpsCert: httpsData.cert,
httpsPort: getUniqueHttpsPort(),
udpPort: getUniqueUdpPort(),
dnssecZone: 'example.com',
udpBindInterface: '127.0.0.1',
httpsBindInterface: '127.0.0.1'
});
// Add timeout to start operation
const startPromise = dnsServer.start();
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Start operation timed out')), 10000);
});
await Promise.race([startPromise, timeoutPromise]);
// @ts-ignore - accessing private property for testing
expect(dnsServer.httpsServer).toBeDefined();
// @ts-ignore - accessing private property for testing
expect(dnsServer.udpServer).toBeDefined();
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('should reject invalid IP addresses', async () => {
const httpsData = await tapNodeTools.createHttpsCert();
// Test invalid UDP interface
dnsServer = new smartdns.DnsServer({
httpsKey: httpsData.key,
httpsCert: httpsData.cert,
httpsPort: getUniqueHttpsPort(),
udpPort: getUniqueUdpPort(),
dnssecZone: 'example.com',
udpBindInterface: 'invalid-ip',
});
let error1 = null;
try {
await dnsServer.start();
} catch (err) {
error1 = err;
}
expect(error1).toBeDefined();
expect(error1.message).toContain('Invalid UDP bind interface');
// Test invalid HTTPS interface
dnsServer = new smartdns.DnsServer({
httpsKey: httpsData.key,
httpsCert: httpsData.cert,
httpsPort: getUniqueHttpsPort(),
udpPort: getUniqueUdpPort(),
dnssecZone: 'example.com',
httpsBindInterface: '999.999.999.999',
});
let error2 = null;
try {
await dnsServer.start();
} catch (err) {
error2 = err;
}
expect(error2).toBeDefined();
expect(error2.message).toContain('Invalid HTTPS bind interface');
dnsServer = null;
});
tap.test('should work with IPv6 localhost if available', async () => {
// Clean up any existing server
await stopServer(dnsServer);
// Skip IPv6 test if not supported
try {
const testSocket = require('dgram').createSocket('udp6');
testSocket.bind(0, '::1');
testSocket.close();
} catch (err) {
console.log('IPv6 not supported in this environment, skipping test');
return;
}
const httpsData = await tapNodeTools.createHttpsCert();
dnsServer = new smartdns.DnsServer({
httpsKey: httpsData.key,
httpsCert: httpsData.cert,
httpsPort: getUniqueHttpsPort(),
udpPort: getUniqueUdpPort(),
dnssecZone: 'example.com',
udpBindInterface: '::1',
httpsBindInterface: '::1'
});
try {
await dnsServer.start();
// @ts-ignore - accessing private property for testing
expect(dnsServer.httpsServer).toBeDefined();
await stopServer(dnsServer);
} catch (err) {
console.log('IPv6 binding failed:', err.message);
await stopServer(dnsServer);
throw err;
}
dnsServer = null;
});
tap.test('should stop the server', async () => { tap.test('should stop the server', async () => {
// Clean up any existing server // Clean up any existing server
await stopServer(dnsServer); await stopServer(dnsServer);

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartdns', name: '@push.rocks/smartdns',
version: '7.0.1', version: '7.4.0',
description: 'A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.' description: 'A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.'
} }

View File

@ -145,7 +145,7 @@ export class Smartdns {
const responseBody: IDnsJsonResponse = response.body; const responseBody: IDnsJsonResponse = response.body;
if (responseBody?.Status !== 0 && counterArg < retriesCounterArg) { if (responseBody?.Status !== 0 && counterArg < retriesCounterArg) {
await plugins.smartdelay.delayFor(500); await plugins.smartdelay.delayFor(500);
return getResponseBody(counterArg++); return getResponseBody(counterArg + 1);
} else { } else {
return responseBody; return responseBody;
} }

View File

@ -8,6 +8,11 @@ export interface IDnsServerOptions {
httpsPort: number; httpsPort: number;
udpPort: number; udpPort: number;
dnssecZone: string; dnssecZone: string;
udpBindInterface?: string;
httpsBindInterface?: string;
// New options for independent manual socket control
manualUdpMode?: boolean;
manualHttpsMode?: boolean;
} }
export interface DnsAnswer { export interface DnsAnswer {
@ -31,18 +36,6 @@ interface DNSKEYData {
key: Buffer; key: Buffer;
} }
interface RRSIGData {
typeCovered: string; // Changed to string to match dns-packet expectations
algorithm: number;
labels: number;
originalTTL: number;
expiration: number;
inception: number;
keyTag: number;
signerName: string;
signature: Buffer;
}
// Let's Encrypt related interfaces // Let's Encrypt related interfaces
interface LetsEncryptOptions { interface LetsEncryptOptions {
email?: string; email?: string;
@ -60,6 +53,10 @@ export class DnsServer {
private dnskeyRecord: DNSKEYData; private dnskeyRecord: DNSKEYData;
private keyTag: number; private keyTag: number;
// Track if servers are initialized
private udpServerInitialized: boolean = false;
private httpsServerInitialized: boolean = false;
constructor(private options: IDnsServerOptions) { constructor(private options: IDnsServerOptions) {
// Initialize DNSSEC // Initialize DNSSEC
this.dnsSec = new DnsSec({ this.dnsSec = new DnsSec({
@ -70,13 +67,125 @@ export class DnsServer {
}); });
// Generate DNSKEY and DS records // Generate DNSKEY and DS records
const { dsRecord, dnskeyRecord } = this.dnsSec.getDsAndKeyPair(); const { dnskeyRecord } = this.dnsSec.getDsAndKeyPair();
// Parse DNSKEY record into dns-packet format // Parse DNSKEY record into dns-packet format
this.dnskeyRecord = this.parseDNSKEYRecord(dnskeyRecord); this.dnskeyRecord = this.parseDNSKEYRecord(dnskeyRecord);
this.keyTag = this.computeKeyTag(this.dnskeyRecord); this.keyTag = this.computeKeyTag(this.dnskeyRecord);
} }
/**
* Initialize servers without binding to ports
* This is called automatically by start() or can be called manually
*/
public initializeServers(): void {
this.initializeUdpServer();
this.initializeHttpsServer();
}
/**
* Initialize UDP server without binding
*/
public initializeUdpServer(): void {
if (this.udpServerInitialized) {
return;
}
// Create UDP socket without binding
const udpInterface = this.options.udpBindInterface || '0.0.0.0';
const socketType = this.isIPv6(udpInterface) ? 'udp6' : 'udp4';
this.udpServer = plugins.dgram.createSocket(socketType);
// Set up UDP message handler
this.udpServer.on('message', (msg, rinfo) => {
this.handleUdpMessage(msg, rinfo);
});
this.udpServer.on('error', (err) => {
console.error(`UDP Server error:\n${err.stack}`);
this.udpServer.close();
});
this.udpServerInitialized = true;
}
/**
* Initialize HTTPS server without binding
*/
public initializeHttpsServer(): void {
if (this.httpsServerInitialized) {
return;
}
// Create HTTPS server without listening
this.httpsServer = plugins.https.createServer(
{
key: this.options.httpsKey,
cert: this.options.httpsCert,
},
this.handleHttpsRequest.bind(this)
);
this.httpsServerInitialized = true;
}
/**
* Handle a raw TCP socket for HTTPS/DoH
* @param socket The TCP socket to handle
*/
public handleHttpsSocket(socket: plugins.net.Socket): void {
if (!this.httpsServer) {
this.initializeHttpsServer();
}
// Emit connection event on the HTTPS server
this.httpsServer.emit('connection', socket);
}
/**
* Handle a UDP message manually
* @param msg The DNS message buffer
* @param rinfo Remote address information
* @param responseCallback Optional callback to handle the response
*/
public handleUdpMessage(
msg: Buffer,
rinfo: plugins.dgram.RemoteInfo,
responseCallback?: (response: Buffer, rinfo: plugins.dgram.RemoteInfo) => void
): void {
try {
const request = dnsPacket.decode(msg);
const response = this.processDnsRequest(request);
const responseData = dnsPacket.encode(response);
if (responseCallback) {
// Use custom callback if provided
responseCallback(responseData, rinfo);
} else if (this.udpServer && !this.options.manualUdpMode) {
// Use the internal UDP server to send response
this.udpServer.send(responseData, rinfo.port, rinfo.address);
}
// In manual mode without callback, caller is responsible for sending response
} catch (err) {
console.error('Error processing UDP DNS request:', err);
}
}
/**
* Process a raw DNS packet and return the response
* This is useful for custom transport implementations
*/
public processRawDnsPacket(packet: Buffer): Buffer {
try {
const request = dnsPacket.decode(packet);
const response = this.processDnsRequest(request);
return dnsPacket.encode(response);
} catch (err) {
console.error('Error processing raw DNS packet:', err);
throw err;
}
}
public registerHandler( public registerHandler(
domainPattern: string, domainPattern: string,
recordTypes: string[], recordTypes: string[],
@ -183,7 +292,7 @@ export class DnsServer {
const domain = auth.identifier.value; const domain = auth.identifier.value;
// Get DNS challenge // Get DNS challenge
const challenge = auth.challenges.find(c => c.type === 'dns-01'); const challenge = auth.challenges.find((c: any) => c.type === 'dns-01');
if (!challenge) { if (!challenge) {
throw new Error(`No DNS-01 challenge found for ${domain}`); throw new Error(`No DNS-01 challenge found for ${domain}`);
} }
@ -265,8 +374,10 @@ export class DnsServer {
this.options.httpsCert = certificate; this.options.httpsCert = certificate;
this.options.httpsKey = privateKey; this.options.httpsKey = privateKey;
// Restart HTTPS server with new certificate // Restart HTTPS server with new certificate (only if not in manual HTTPS mode)
if (!this.options.manualHttpsMode) {
await this.restartHttpsServer(); await this.restartHttpsServer();
}
// Clean up challenge handlers // Clean up challenge handlers
for (const handler of challengeHandlers) { for (const handler of challengeHandlers) {
@ -334,10 +445,15 @@ export class DnsServer {
this.handleHttpsRequest.bind(this) this.handleHttpsRequest.bind(this)
); );
this.httpsServer.listen(this.options.httpsPort, () => { if (!this.options.manualHttpsMode) {
console.log(`HTTPS DNS server restarted on port ${this.options.httpsPort} with test certificate`); const httpsInterface = this.options.httpsBindInterface || '0.0.0.0';
this.httpsServer.listen(this.options.httpsPort, httpsInterface, () => {
console.log(`HTTPS DNS server restarted on ${httpsInterface}:${this.options.httpsPort} with test certificate`);
resolve(); resolve();
}); });
} else {
resolve();
}
return; return;
} }
} }
@ -351,10 +467,15 @@ export class DnsServer {
this.handleHttpsRequest.bind(this) this.handleHttpsRequest.bind(this)
); );
this.httpsServer.listen(this.options.httpsPort, () => { if (!this.options.manualHttpsMode) {
console.log(`HTTPS DNS server restarted on port ${this.options.httpsPort} with new certificate`); const httpsInterface = this.options.httpsBindInterface || '0.0.0.0';
this.httpsServer.listen(this.options.httpsPort, httpsInterface, () => {
console.log(`HTTPS DNS server restarted on ${httpsInterface}:${this.options.httpsPort} with new certificate`);
resolve(); resolve();
}); });
} else {
resolve();
}
} catch (err) { } catch (err) {
console.error('Error creating HTTPS server with new certificate:', err); console.error('Error creating HTTPS server with new certificate:', err);
reject(err); reject(err);
@ -386,6 +507,25 @@ export class DnsServer {
return authorizedDomains; return authorizedDomains;
} }
/**
* Validate if a string is a valid IP address (IPv4 or IPv6)
*/
private isValidIpAddress(ip: string): boolean {
// IPv4 pattern
const ipv4Pattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// IPv6 pattern (simplified but more comprehensive)
const ipv6Pattern = /^(::1|::)$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
return ipv4Pattern.test(ip) || ipv6Pattern.test(ip);
}
/**
* Determine if an IP address is IPv6
*/
private isIPv6(ip: string): boolean {
return ip.includes(':');
}
/** /**
* Check if the server is authoritative for a domain * Check if the server is authoritative for a domain
*/ */
@ -683,45 +823,76 @@ export class DnsServer {
} }
public async start(): Promise<void> { public async start(): Promise<void> {
this.httpsServer = plugins.https.createServer( // Initialize servers based on what's needed
{ if (!this.options.manualUdpMode) {
key: this.options.httpsKey, this.initializeUdpServer();
cert: this.options.httpsCert, }
}, if (!this.options.manualHttpsMode) {
this.handleHttpsRequest.bind(this) this.initializeHttpsServer();
); }
this.udpServer = plugins.dgram.createSocket('udp4'); // Handle different mode combinations
this.udpServer.on('message', (msg, rinfo) => { const udpManual = this.options.manualUdpMode || false;
const request = dnsPacket.decode(msg); const httpsManual = this.options.manualHttpsMode || false;
const response = this.processDnsRequest(request);
const responseData = dnsPacket.encode(response);
this.udpServer.send(responseData, rinfo.port, rinfo.address);
});
this.udpServer.on('error', (err) => { if (udpManual && httpsManual) {
console.error(`UDP Server error:\n${err.stack}`); console.log('DNS server started in full manual mode - ready to accept connections');
this.udpServer.close(); return;
}); } else if (udpManual && !httpsManual) {
console.log('DNS server started with manual UDP mode and automatic HTTPS binding');
} else if (!udpManual && httpsManual) {
console.log('DNS server started with automatic UDP binding and manual HTTPS mode');
}
// Validate interface addresses if provided
const udpInterface = this.options.udpBindInterface || '0.0.0.0';
const httpsInterface = this.options.httpsBindInterface || '0.0.0.0';
if (this.options.udpBindInterface && !this.isValidIpAddress(this.options.udpBindInterface)) {
throw new Error(`Invalid UDP bind interface: ${this.options.udpBindInterface}`);
}
if (this.options.httpsBindInterface && !this.isValidIpAddress(this.options.httpsBindInterface)) {
throw new Error(`Invalid HTTPS bind interface: ${this.options.httpsBindInterface}`);
}
const promises: Promise<void>[] = [];
// Bind UDP if not in manual UDP mode
if (!udpManual) {
const udpListeningDeferred = plugins.smartpromise.defer<void>(); const udpListeningDeferred = plugins.smartpromise.defer<void>();
const httpsListeningDeferred = plugins.smartpromise.defer<void>(); promises.push(udpListeningDeferred.promise);
try { try {
this.udpServer.bind(this.options.udpPort, '0.0.0.0', () => { this.udpServer.bind(this.options.udpPort, udpInterface, () => {
console.log(`UDP DNS server running on port ${this.options.udpPort}`); console.log(`UDP DNS server running on ${udpInterface}:${this.options.udpPort}`);
udpListeningDeferred.resolve(); udpListeningDeferred.resolve();
}); });
} catch (err) {
console.error('Error starting UDP DNS server:', err);
udpListeningDeferred.reject(err);
}
}
this.httpsServer.listen(this.options.httpsPort, () => { // Bind HTTPS if not in manual HTTPS mode
console.log(`HTTPS DNS server running on port ${this.options.httpsPort}`); if (!httpsManual) {
const httpsListeningDeferred = plugins.smartpromise.defer<void>();
promises.push(httpsListeningDeferred.promise);
try {
this.httpsServer.listen(this.options.httpsPort, httpsInterface, () => {
console.log(`HTTPS DNS server running on ${httpsInterface}:${this.options.httpsPort}`);
httpsListeningDeferred.resolve(); httpsListeningDeferred.resolve();
}); });
} catch (err) { } catch (err) {
console.error('Error starting DNS server:', err); console.error('Error starting HTTPS DNS server:', err);
process.exit(1); httpsListeningDeferred.reject(err);
}
}
if (promises.length > 0) {
await Promise.all(promises);
} }
await Promise.all([udpListeningDeferred.promise, httpsListeningDeferred.promise]);
} }
public async stop(): Promise<void> { public async stop(): Promise<void> {
@ -755,6 +926,8 @@ export class DnsServer {
} }
await Promise.all([doneUdp.promise, doneHttps.promise]); await Promise.all([doneUdp.promise, doneHttps.promise]);
this.udpServerInitialized = false;
this.httpsServerInitialized = false;
} }
// Helper methods // Helper methods

View File

@ -4,14 +4,16 @@ import dgram from 'dgram';
import fs from 'fs'; import fs from 'fs';
import http from 'http'; import http from 'http';
import https from 'https'; import https from 'https';
import * as net from 'net';
import * as path from 'path'; import * as path from 'path';
export { export {
crypto, crypto,
dgram,
fs, fs,
http, http,
https, https,
dgram, net,
path, path,
} }