Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
3094c9d06c | |||
62b6fa26fa | |||
46e51cd846 | |||
dd12641fb0 |
18
changelog.md
18
changelog.md
@ -1,5 +1,23 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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)
|
## 2025-05-27 - 7.1.0 - feat(docs)
|
||||||
Improve documentation for advanced DNS features and update usage examples for both DNS client and server.
|
Improve documentation for advanced DNS features and update usage examples for both DNS client and server.
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartdns",
|
"name": "@push.rocks/smartdns",
|
||||||
"version": "7.1.0",
|
"version": "7.3.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"
|
||||||
},
|
},
|
||||||
|
61
readme.md
61
readme.md
@ -190,6 +190,17 @@ const dnsServer = new DnsServer({
|
|||||||
dnssecZone: 'example.com' // Optional: enable DNSSEC for this zone
|
dnssecZone: 'example.com' // Optional: enable DNSSEC for this zone
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// For enhanced security, bind to specific interfaces
|
||||||
|
const secureServer = new DnsServer({
|
||||||
|
udpPort: 53,
|
||||||
|
httpsPort: 443,
|
||||||
|
httpsKey: 'path/to/key.pem',
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
// Register a handler for all subdomains of example.com
|
// Register a handler for all subdomains of example.com
|
||||||
dnsServer.registerHandler('*.example.com', ['A'], (question) => ({
|
dnsServer.registerHandler('*.example.com', ['A'], (question) => ({
|
||||||
name: question.name,
|
name: question.name,
|
||||||
@ -363,6 +374,56 @@ await dnsServer.start();
|
|||||||
// https://localhost:8443/dns-query
|
// https://localhost:8443/dns-query
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Interface Binding
|
||||||
|
|
||||||
|
For enhanced security and network isolation, you can bind the DNS server to specific network interfaces instead of all available interfaces.
|
||||||
|
|
||||||
|
#### Localhost-Only Binding
|
||||||
|
|
||||||
|
Bind to localhost for development or local-only DNS services:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const localServer = new DnsServer({
|
||||||
|
udpPort: 5353,
|
||||||
|
httpsPort: 8443,
|
||||||
|
httpsKey: cert.key,
|
||||||
|
httpsCert: cert.cert,
|
||||||
|
dnssecZone: 'local.test',
|
||||||
|
udpBindInterface: '127.0.0.1', // IPv4 localhost
|
||||||
|
httpsBindInterface: '127.0.0.1'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Or use IPv6 localhost
|
||||||
|
const ipv6LocalServer = new DnsServer({
|
||||||
|
// ... other options
|
||||||
|
udpBindInterface: '::1', // IPv6 localhost
|
||||||
|
httpsBindInterface: '::1'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Specific Interface Binding
|
||||||
|
|
||||||
|
Bind to a specific network interface in multi-homed servers:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const interfaceServer = new DnsServer({
|
||||||
|
udpPort: 53,
|
||||||
|
httpsPort: 443,
|
||||||
|
httpsKey: cert.key,
|
||||||
|
httpsCert: cert.cert,
|
||||||
|
dnssecZone: 'example.com',
|
||||||
|
udpBindInterface: '192.168.1.100', // Specific internal interface
|
||||||
|
httpsBindInterface: '10.0.0.50' // Different interface for HTTPS
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Security Considerations
|
||||||
|
|
||||||
|
- **Default Behavior**: If not specified, servers bind to all interfaces (`0.0.0.0`)
|
||||||
|
- **Localhost Binding**: Use `127.0.0.1` or `::1` for development and testing
|
||||||
|
- **Production**: Consider binding to specific internal interfaces for security
|
||||||
|
- **Validation**: Invalid IP addresses will throw an error during server startup
|
||||||
|
|
||||||
### Advanced Handler Patterns
|
### Advanced Handler Patterns
|
||||||
|
|
||||||
#### Pattern-Based Routing
|
#### Pattern-Based Routing
|
||||||
|
103
readme.plan.md
Normal file
103
readme.plan.md
Normal 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
|
@ -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);
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartdns',
|
name: '@push.rocks/smartdns',
|
||||||
version: '7.1.0',
|
version: '7.3.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.'
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
await this.restartHttpsServer();
|
if (!this.options.manualHttpsMode) {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
resolve();
|
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();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
resolve();
|
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);
|
if (udpManual && httpsManual) {
|
||||||
this.udpServer.send(responseData, rinfo.port, rinfo.address);
|
console.log('DNS server started in full manual mode - ready to accept connections');
|
||||||
});
|
return;
|
||||||
|
} else if (udpManual && !httpsManual) {
|
||||||
this.udpServer.on('error', (err) => {
|
console.log('DNS server started with manual UDP mode and automatic HTTPS binding');
|
||||||
console.error(`UDP Server error:\n${err.stack}`);
|
} else if (!udpManual && httpsManual) {
|
||||||
this.udpServer.close();
|
console.log('DNS server started with automatic UDP binding and manual HTTPS mode');
|
||||||
});
|
}
|
||||||
|
|
||||||
const udpListeningDeferred = plugins.smartpromise.defer<void>();
|
// Validate interface addresses if provided
|
||||||
const httpsListeningDeferred = plugins.smartpromise.defer<void>();
|
const udpInterface = this.options.udpBindInterface || '0.0.0.0';
|
||||||
|
const httpsInterface = this.options.httpsBindInterface || '0.0.0.0';
|
||||||
try {
|
|
||||||
this.udpServer.bind(this.options.udpPort, '0.0.0.0', () => {
|
if (this.options.udpBindInterface && !this.isValidIpAddress(this.options.udpBindInterface)) {
|
||||||
console.log(`UDP DNS server running on port ${this.options.udpPort}`);
|
throw new Error(`Invalid UDP bind interface: ${this.options.udpBindInterface}`);
|
||||||
udpListeningDeferred.resolve();
|
}
|
||||||
});
|
|
||||||
|
if (this.options.httpsBindInterface && !this.isValidIpAddress(this.options.httpsBindInterface)) {
|
||||||
this.httpsServer.listen(this.options.httpsPort, () => {
|
throw new Error(`Invalid HTTPS bind interface: ${this.options.httpsBindInterface}`);
|
||||||
console.log(`HTTPS DNS server running on port ${this.options.httpsPort}`);
|
}
|
||||||
httpsListeningDeferred.resolve();
|
|
||||||
});
|
const promises: Promise<void>[] = [];
|
||||||
} catch (err) {
|
|
||||||
console.error('Error starting DNS server:', err);
|
// Bind UDP if not in manual UDP mode
|
||||||
process.exit(1);
|
if (!udpManual) {
|
||||||
|
const udpListeningDeferred = plugins.smartpromise.defer<void>();
|
||||||
|
promises.push(udpListeningDeferred.promise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.udpServer.bind(this.options.udpPort, udpInterface, () => {
|
||||||
|
console.log(`UDP DNS server running on ${udpInterface}:${this.options.udpPort}`);
|
||||||
|
udpListeningDeferred.resolve();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error starting UDP DNS server:', err);
|
||||||
|
udpListeningDeferred.reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind HTTPS if not in manual HTTPS mode
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error starting HTTPS DNS server:', err);
|
||||||
|
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
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user