feat(dns-server): Improve DNS server interface binding by adding explicit IP validation, configurable UDP/HTTPS binding, and enhanced logging.
This commit is contained in:
@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
@ -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
|
211
readme.plan2.md
Normal file
211
readme.plan2.md
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
# DNS Server Manual Socket Handling Implementation Plan
|
||||||
|
|
||||||
|
Command to reread CLAUDE.md: `cat /home/philkunz/.claude/CLAUDE.md`
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Enable manual socket handling for the DNSServer class to allow external socket management instead of internal socket creation and binding. This enables advanced use cases like socket pooling, custom networking layers, and integration with existing socket infrastructures.
|
||||||
|
|
||||||
|
## Current Implementation Analysis
|
||||||
|
- DNSServer currently creates and manages its own UDP (`dgram.Socket`) and HTTPS (`https.Server`) sockets
|
||||||
|
- Socket creation happens in `start()` method at lines ~728-752 in `ts_server/classes.dnsserver.ts`
|
||||||
|
- No mechanism exists to inject pre-created sockets
|
||||||
|
- Socket lifecycle is tightly coupled with server lifecycle
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### 1. Update IDnsServerOptions Interface
|
||||||
|
- Add optional `udpSocket?: dgram.Socket` property
|
||||||
|
- Add optional `httpsServer?: https.Server` property
|
||||||
|
- Add optional `manualMode?: boolean` flag to indicate manual socket handling
|
||||||
|
- Maintain backwards compatibility with existing automatic socket creation
|
||||||
|
|
||||||
|
### 2. Modify DnsServer Constructor
|
||||||
|
- Accept pre-created sockets in options
|
||||||
|
- Store references to manual sockets
|
||||||
|
- Set internal flags to track manual vs automatic mode
|
||||||
|
|
||||||
|
### 3. Update start() Method Logic
|
||||||
|
- Skip socket creation if manual sockets provided
|
||||||
|
- Validate manual sockets are in correct state
|
||||||
|
- Attach event listeners to provided sockets
|
||||||
|
- Support hybrid mode (UDP manual, HTTPS automatic, or vice versa)
|
||||||
|
|
||||||
|
### 4. Update stop() Method Logic
|
||||||
|
- For manual sockets: only remove event listeners, don't close sockets
|
||||||
|
- For automatic sockets: close sockets as current behavior
|
||||||
|
- Provide clear separation of lifecycle management
|
||||||
|
|
||||||
|
### 5. Add Socket Validation
|
||||||
|
- Verify provided UDP socket is correct type (udp4/udp6)
|
||||||
|
- Ensure HTTPS server is properly configured
|
||||||
|
- Validate socket state (not already listening, etc.)
|
||||||
|
|
||||||
|
### 6. Error Handling & Events
|
||||||
|
- Emit events for socket errors without stopping server
|
||||||
|
- Allow graceful handling of external socket failures
|
||||||
|
- Provide clear error messages for socket state issues
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
- **Socket Pooling**: Share sockets across multiple DNS server instances
|
||||||
|
- **Custom Networking**: Integration with custom network stacks
|
||||||
|
- **Performance**: Reuse existing socket infrastructure
|
||||||
|
- **Advanced Configuration**: Fine-grained control over socket options
|
||||||
|
- **Testing**: Easier mocking and testing with provided sockets
|
||||||
|
- **Container Integration**: Better integration with orchestration platforms
|
||||||
|
|
||||||
|
## Example Usage After Implementation
|
||||||
|
|
||||||
|
### Manual UDP Socket with Automatic HTTPS
|
||||||
|
```typescript
|
||||||
|
import * as dgram from 'dgram';
|
||||||
|
import { DnsServer } from '@push.rocks/smartdns/server';
|
||||||
|
|
||||||
|
// Create and configure UDP socket externally
|
||||||
|
const udpSocket = dgram.createSocket('udp4');
|
||||||
|
udpSocket.bind(53, '0.0.0.0');
|
||||||
|
|
||||||
|
const dnsServer = new DnsServer({
|
||||||
|
// Manual UDP socket
|
||||||
|
udpSocket: udpSocket,
|
||||||
|
manualMode: true,
|
||||||
|
|
||||||
|
// Automatic HTTPS server
|
||||||
|
httpsPort: 443,
|
||||||
|
httpsKey: cert.key,
|
||||||
|
httpsCert: cert.cert,
|
||||||
|
dnssecZone: 'example.com'
|
||||||
|
});
|
||||||
|
|
||||||
|
await dnsServer.start(); // Won't create new UDP socket
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fully Manual Mode
|
||||||
|
```typescript
|
||||||
|
import * as https from 'https';
|
||||||
|
import * as dgram from 'dgram';
|
||||||
|
|
||||||
|
// Create both sockets externally
|
||||||
|
const udpSocket = dgram.createSocket('udp4');
|
||||||
|
const httpsServer = https.createServer({ key: cert.key, cert: cert.cert });
|
||||||
|
|
||||||
|
udpSocket.bind(53, '0.0.0.0');
|
||||||
|
httpsServer.listen(443, '0.0.0.0');
|
||||||
|
|
||||||
|
const dnsServer = new DnsServer({
|
||||||
|
udpSocket: udpSocket,
|
||||||
|
httpsServer: httpsServer,
|
||||||
|
manualMode: true,
|
||||||
|
dnssecZone: 'example.com'
|
||||||
|
});
|
||||||
|
|
||||||
|
await dnsServer.start(); // Only attaches handlers, no socket creation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Socket Pooling Example
|
||||||
|
```typescript
|
||||||
|
// Shared socket pool
|
||||||
|
const socketPool = {
|
||||||
|
udp: dgram.createSocket('udp4'),
|
||||||
|
https: https.createServer(sslOptions)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Multiple DNS servers sharing sockets
|
||||||
|
const server1 = new DnsServer({
|
||||||
|
udpSocket: socketPool.udp,
|
||||||
|
httpsServer: socketPool.https,
|
||||||
|
manualMode: true,
|
||||||
|
dnssecZone: 'zone1.com'
|
||||||
|
});
|
||||||
|
|
||||||
|
const server2 = new DnsServer({
|
||||||
|
udpSocket: socketPool.udp,
|
||||||
|
httpsServer: socketPool.https,
|
||||||
|
manualMode: true,
|
||||||
|
dnssecZone: 'zone2.com'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### New Interface Properties
|
||||||
|
```typescript
|
||||||
|
export interface IDnsServerOptions {
|
||||||
|
// Existing properties...
|
||||||
|
httpsKey: string;
|
||||||
|
httpsCert: string;
|
||||||
|
httpsPort: number;
|
||||||
|
udpPort: number;
|
||||||
|
dnssecZone: string;
|
||||||
|
udpBindInterface?: string;
|
||||||
|
httpsBindInterface?: string;
|
||||||
|
|
||||||
|
// New manual socket properties
|
||||||
|
udpSocket?: plugins.dgram.Socket;
|
||||||
|
httpsServer?: plugins.https.Server;
|
||||||
|
manualMode?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modified Constructor Logic
|
||||||
|
```typescript
|
||||||
|
constructor(private options: IDnsServerOptions) {
|
||||||
|
// Existing DNSSEC initialization...
|
||||||
|
|
||||||
|
// Handle manual sockets
|
||||||
|
if (options.manualMode) {
|
||||||
|
if (options.udpSocket) {
|
||||||
|
this.udpServer = options.udpSocket;
|
||||||
|
}
|
||||||
|
if (options.httpsServer) {
|
||||||
|
this.httpsServer = options.httpsServer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updated start() Method
|
||||||
|
```typescript
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
// Manual socket mode handling
|
||||||
|
if (this.options.manualMode) {
|
||||||
|
await this.startManualMode();
|
||||||
|
} else {
|
||||||
|
await this.startAutomaticMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startManualMode(): Promise<void> {
|
||||||
|
if (this.udpServer) {
|
||||||
|
this.attachUdpHandlers();
|
||||||
|
}
|
||||||
|
if (this.httpsServer) {
|
||||||
|
this.attachHttpsHandlers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
1. `ts_server/classes.dnsserver.ts` - Core implementation
|
||||||
|
2. `test/test.server.ts` - Add manual socket tests
|
||||||
|
3. `readme.md` - Documentation updates with examples
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
- Unit tests for manual socket validation
|
||||||
|
- Integration tests for mixed manual/automatic mode
|
||||||
|
- Socket lifecycle tests (start/stop behavior)
|
||||||
|
- Error handling tests for invalid socket states
|
||||||
|
- Performance tests comparing manual vs automatic modes
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
- Feature is completely backwards compatible
|
||||||
|
- Existing code continues to work unchanged
|
||||||
|
- New `manualMode` flag clearly indicates intent
|
||||||
|
- Gradual adoption possible (UDP manual, HTTPS automatic, etc.)
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
- Validate provided sockets are properly configured
|
||||||
|
- Ensure manual sockets have appropriate permissions
|
||||||
|
- Document security implications of socket sharing
|
||||||
|
- Provide guidance on proper socket isolation
|
||||||
|
|
||||||
|
This enhancement enables advanced DNS server deployments while maintaining full backwards compatibility with existing implementations.
|
@ -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.2.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,8 @@ export interface IDnsServerOptions {
|
|||||||
httpsPort: number;
|
httpsPort: number;
|
||||||
udpPort: number;
|
udpPort: number;
|
||||||
dnssecZone: string;
|
dnssecZone: string;
|
||||||
|
udpBindInterface?: string;
|
||||||
|
httpsBindInterface?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DnsAnswer {
|
export interface DnsAnswer {
|
||||||
@ -31,17 +33,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 {
|
||||||
@ -70,7 +61,7 @@ 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);
|
||||||
@ -183,7 +174,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}`);
|
||||||
}
|
}
|
||||||
@ -334,8 +325,9 @@ export class DnsServer {
|
|||||||
this.handleHttpsRequest.bind(this)
|
this.handleHttpsRequest.bind(this)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.httpsServer.listen(this.options.httpsPort, () => {
|
const httpsInterface = this.options.httpsBindInterface || '0.0.0.0';
|
||||||
console.log(`HTTPS DNS server restarted on port ${this.options.httpsPort} with test certificate`);
|
this.httpsServer.listen(this.options.httpsPort, httpsInterface, () => {
|
||||||
|
console.log(`HTTPS DNS server restarted on ${httpsInterface}:${this.options.httpsPort} with test certificate`);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -351,8 +343,9 @@ export class DnsServer {
|
|||||||
this.handleHttpsRequest.bind(this)
|
this.handleHttpsRequest.bind(this)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.httpsServer.listen(this.options.httpsPort, () => {
|
const httpsInterface = this.options.httpsBindInterface || '0.0.0.0';
|
||||||
console.log(`HTTPS DNS server restarted on port ${this.options.httpsPort} with new certificate`);
|
this.httpsServer.listen(this.options.httpsPort, httpsInterface, () => {
|
||||||
|
console.log(`HTTPS DNS server restarted on ${httpsInterface}:${this.options.httpsPort} with new certificate`);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -386,6 +379,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,6 +695,18 @@ export class DnsServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
|
// 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}`);
|
||||||
|
}
|
||||||
|
|
||||||
this.httpsServer = plugins.https.createServer(
|
this.httpsServer = plugins.https.createServer(
|
||||||
{
|
{
|
||||||
key: this.options.httpsKey,
|
key: this.options.httpsKey,
|
||||||
@ -691,7 +715,9 @@ export class DnsServer {
|
|||||||
this.handleHttpsRequest.bind(this)
|
this.handleHttpsRequest.bind(this)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.udpServer = plugins.dgram.createSocket('udp4');
|
// Create appropriate socket type based on interface
|
||||||
|
const socketType = this.isIPv6(udpInterface) ? 'udp6' : 'udp4';
|
||||||
|
this.udpServer = plugins.dgram.createSocket(socketType);
|
||||||
this.udpServer.on('message', (msg, rinfo) => {
|
this.udpServer.on('message', (msg, rinfo) => {
|
||||||
const request = dnsPacket.decode(msg);
|
const request = dnsPacket.decode(msg);
|
||||||
const response = this.processDnsRequest(request);
|
const response = this.processDnsRequest(request);
|
||||||
@ -708,13 +734,13 @@ export class DnsServer {
|
|||||||
const httpsListeningDeferred = plugins.smartpromise.defer<void>();
|
const httpsListeningDeferred = plugins.smartpromise.defer<void>();
|
||||||
|
|
||||||
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.httpsServer.listen(this.options.httpsPort, () => {
|
this.httpsServer.listen(this.options.httpsPort, httpsInterface, () => {
|
||||||
console.log(`HTTPS DNS server running on port ${this.options.httpsPort}`);
|
console.log(`HTTPS DNS server running on ${httpsInterface}:${this.options.httpsPort}`);
|
||||||
httpsListeningDeferred.resolve();
|
httpsListeningDeferred.resolve();
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
Reference in New Issue
Block a user