Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
1185ea67d4 | |||
b187da507b | |||
3094c9d06c | |||
62b6fa26fa | |||
46e51cd846 | |||
dd12641fb0 |
25
changelog.md
25
changelog.md
@ -1,5 +1,30 @@
|
||||
# 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.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartdns",
|
||||
"version": "7.1.0",
|
||||
"version": "7.4.0",
|
||||
"private": false,
|
||||
"description": "A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.",
|
||||
"exports": {
|
||||
@ -9,7 +9,7 @@
|
||||
"./client": "./dist_ts_client/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "(tstest test/)",
|
||||
"test": "(tstest test/ --verbose --timeout 60)",
|
||||
"build": "(tsbuild tsfolders --web --allowimplicitany)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
|
274
readme.md
274
readme.md
@ -190,6 +190,17 @@ const dnsServer = new DnsServer({
|
||||
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
|
||||
dnsServer.registerHandler('*.example.com', ['A'], (question) => ({
|
||||
name: question.name,
|
||||
@ -295,6 +306,215 @@ await dnsServer.start();
|
||||
console.log('DNS Server with Let\'s Encrypt SSL started!');
|
||||
```
|
||||
|
||||
### Manual Socket Handling
|
||||
|
||||
The DNS server supports manual socket handling for advanced use cases like clustering, load balancing, and custom transport implementations. You can control UDP and HTTPS socket handling independently.
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
```typescript
|
||||
export interface IDnsServerOptions {
|
||||
// ... standard options ...
|
||||
manualUdpMode?: boolean; // Handle UDP sockets manually
|
||||
manualHttpsMode?: boolean; // Handle HTTPS sockets manually
|
||||
}
|
||||
```
|
||||
|
||||
#### Basic Manual Socket Usage
|
||||
|
||||
```typescript
|
||||
import { DnsServer } from '@push.rocks/smartdns/server';
|
||||
import * as dgram from 'dgram';
|
||||
import * as net from 'net';
|
||||
|
||||
// Create server with manual UDP mode
|
||||
const dnsServer = new DnsServer({
|
||||
httpsKey: '...',
|
||||
httpsCert: '...',
|
||||
httpsPort: 853,
|
||||
udpPort: 53,
|
||||
dnssecZone: 'example.com',
|
||||
manualUdpMode: true // UDP manual, HTTPS automatic
|
||||
});
|
||||
|
||||
await dnsServer.start(); // HTTPS binds, UDP doesn't
|
||||
|
||||
// Create your own UDP socket
|
||||
const udpSocket = dgram.createSocket('udp4');
|
||||
|
||||
// Handle incoming UDP messages
|
||||
udpSocket.on('message', (msg, rinfo) => {
|
||||
dnsServer.handleUdpMessage(msg, rinfo, (response, responseRinfo) => {
|
||||
// Send response using your socket
|
||||
udpSocket.send(response, responseRinfo.port, responseRinfo.address);
|
||||
});
|
||||
});
|
||||
|
||||
// Bind to custom port or multiple interfaces
|
||||
udpSocket.bind(5353, '0.0.0.0');
|
||||
```
|
||||
|
||||
#### Manual HTTPS Socket Handling
|
||||
|
||||
```typescript
|
||||
// Create server with manual HTTPS mode
|
||||
const dnsServer = new DnsServer({
|
||||
httpsKey: '...',
|
||||
httpsCert: '...',
|
||||
httpsPort: 853,
|
||||
udpPort: 53,
|
||||
dnssecZone: 'example.com',
|
||||
manualHttpsMode: true // HTTPS manual, UDP automatic
|
||||
});
|
||||
|
||||
await dnsServer.start(); // UDP binds, HTTPS doesn't
|
||||
|
||||
// Create your own TCP server
|
||||
const tcpServer = net.createServer((socket) => {
|
||||
// Pass TCP sockets to DNS server
|
||||
dnsServer.handleHttpsSocket(socket);
|
||||
});
|
||||
|
||||
tcpServer.listen(8853, '0.0.0.0');
|
||||
```
|
||||
|
||||
#### Full Manual Mode
|
||||
|
||||
Control both protocols manually for complete flexibility:
|
||||
|
||||
```typescript
|
||||
const dnsServer = new DnsServer({
|
||||
httpsKey: '...',
|
||||
httpsCert: '...',
|
||||
httpsPort: 853,
|
||||
udpPort: 53,
|
||||
dnssecZone: 'example.com',
|
||||
manualUdpMode: true,
|
||||
manualHttpsMode: true
|
||||
});
|
||||
|
||||
await dnsServer.start(); // Neither protocol binds
|
||||
|
||||
// Set up your own socket handling for both protocols
|
||||
// Perfect for custom routing, load balancing, or clustering
|
||||
```
|
||||
|
||||
#### Advanced Use Cases
|
||||
|
||||
##### Load Balancing Across Multiple UDP Sockets
|
||||
|
||||
```typescript
|
||||
// Create multiple UDP sockets for different CPU cores
|
||||
const sockets = [];
|
||||
const numCPUs = require('os').cpus().length;
|
||||
|
||||
for (let i = 0; i < numCPUs; i++) {
|
||||
const socket = dgram.createSocket({
|
||||
type: 'udp4',
|
||||
reuseAddr: true // Allow multiple sockets on same port
|
||||
});
|
||||
|
||||
socket.on('message', (msg, rinfo) => {
|
||||
dnsServer.handleUdpMessage(msg, rinfo, (response, rinfo) => {
|
||||
socket.send(response, rinfo.port, rinfo.address);
|
||||
});
|
||||
});
|
||||
|
||||
socket.bind(53);
|
||||
sockets.push(socket);
|
||||
}
|
||||
```
|
||||
|
||||
##### Clustering with Worker Processes
|
||||
|
||||
```typescript
|
||||
import cluster from 'cluster';
|
||||
import { DnsServer } from '@push.rocks/smartdns/server';
|
||||
|
||||
if (cluster.isPrimary) {
|
||||
// Master process accepts connections
|
||||
const server = net.createServer({ pauseOnConnect: true });
|
||||
|
||||
// Distribute connections to workers
|
||||
server.on('connection', (socket) => {
|
||||
const worker = getNextWorker(); // Round-robin or custom logic
|
||||
worker.send('socket', socket);
|
||||
});
|
||||
|
||||
server.listen(853);
|
||||
} else {
|
||||
// Worker process handles DNS
|
||||
const dnsServer = new DnsServer({
|
||||
httpsKey: '...',
|
||||
httpsCert: '...',
|
||||
httpsPort: 853,
|
||||
udpPort: 53,
|
||||
dnssecZone: 'example.com',
|
||||
manualHttpsMode: true
|
||||
});
|
||||
|
||||
process.on('message', (msg, socket) => {
|
||||
if (msg === 'socket') {
|
||||
dnsServer.handleHttpsSocket(socket);
|
||||
}
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
}
|
||||
```
|
||||
|
||||
##### Custom Transport Protocol
|
||||
|
||||
```typescript
|
||||
// Use DNS server with custom transport (e.g., WebSocket)
|
||||
import WebSocket from 'ws';
|
||||
|
||||
const wss = new WebSocket.Server({ port: 8080 });
|
||||
const dnsServer = new DnsServer({
|
||||
httpsKey: '...',
|
||||
httpsCert: '...',
|
||||
httpsPort: 853,
|
||||
udpPort: 53,
|
||||
dnssecZone: 'example.com',
|
||||
manualUdpMode: true,
|
||||
manualHttpsMode: true
|
||||
});
|
||||
|
||||
await dnsServer.start();
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
ws.on('message', (data) => {
|
||||
// Process DNS query from WebSocket
|
||||
const response = dnsServer.processRawDnsPacket(Buffer.from(data));
|
||||
ws.send(response);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
##### Multi-Interface Binding
|
||||
|
||||
```typescript
|
||||
// Bind to multiple network interfaces manually
|
||||
const interfaces = [
|
||||
{ address: '192.168.1.100', type: 'udp4' },
|
||||
{ address: '10.0.0.50', type: 'udp4' },
|
||||
{ address: '::1', type: 'udp6' }
|
||||
];
|
||||
|
||||
interfaces.forEach(({ address, type }) => {
|
||||
const socket = dgram.createSocket(type);
|
||||
|
||||
socket.on('message', (msg, rinfo) => {
|
||||
console.log(`Query received on ${address}`);
|
||||
dnsServer.handleUdpMessage(msg, rinfo, (response, rinfo) => {
|
||||
socket.send(response, rinfo.port, rinfo.address);
|
||||
});
|
||||
});
|
||||
|
||||
socket.bind(53, address);
|
||||
});
|
||||
```
|
||||
|
||||
### Handling Different Protocols
|
||||
|
||||
#### UDP DNS Server
|
||||
@ -363,6 +583,56 @@ await dnsServer.start();
|
||||
// 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
|
||||
|
||||
#### Pattern-Based Routing
|
||||
@ -512,6 +782,7 @@ await tap.start();
|
||||
5. **Monitoring**: Log DNS queries for debugging and analytics
|
||||
6. **Rate Limiting**: Implement rate limiting for public DNS servers
|
||||
7. **Caching**: Respect TTL values and implement proper caching
|
||||
8. **Manual Sockets**: Use manual socket handling for clustering and load balancing
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
@ -519,6 +790,7 @@ await tap.start();
|
||||
- The DNS server handles concurrent UDP and HTTPS requests efficiently
|
||||
- DNSSEC signatures are generated on-demand to reduce memory usage
|
||||
- Pattern matching uses caching for improved performance
|
||||
- Manual socket handling enables horizontal scaling across CPU cores
|
||||
|
||||
### Security Considerations
|
||||
|
||||
@ -528,6 +800,8 @@ await tap.start();
|
||||
- Implement access controls for DNS server handlers
|
||||
- Use Let's Encrypt for automatic SSL certificate management
|
||||
- Never expose internal network information through DNS
|
||||
- Bind to specific interfaces in production environments
|
||||
- Use manual socket handling for custom security layers
|
||||
|
||||
This comprehensive library provides everything needed for both DNS client operations and running production-grade DNS servers with modern security features in TypeScript.
|
||||
|
||||
|
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 {
|
||||
// Access private properties for checking before stopping
|
||||
// @ts-ignore - accessing private properties for testing
|
||||
const hasHttpsServer = server.httpsServer !== undefined && server.httpsServer !== null;
|
||||
// @ts-ignore - accessing private properties for testing
|
||||
const hasUdpServer = server.udpServer !== undefined && server.udpServer !== null;
|
||||
// Set a timeout for stop operation
|
||||
const stopPromise = server.stop();
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Stop operation timed out')), 5000);
|
||||
});
|
||||
|
||||
// Only try to stop if there's something to stop
|
||||
if (hasHttpsServer || hasUdpServer) {
|
||||
await server.stop();
|
||||
}
|
||||
await Promise.race([stopPromise, timeoutPromise]);
|
||||
} catch (e) {
|
||||
console.log('Handled error when stopping server:', e);
|
||||
// Ignore errors during cleanup
|
||||
console.log('Handled error when stopping server:', e.message || e);
|
||||
|
||||
// Force close if normal stop fails
|
||||
try {
|
||||
// @ts-ignore - accessing private properties for emergency cleanup
|
||||
if (server.httpsServer) {
|
||||
server.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);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
// Clean up any existing server
|
||||
await stopServer(dnsServer);
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartdns',
|
||||
version: '7.1.0',
|
||||
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.'
|
||||
}
|
||||
|
@ -8,6 +8,11 @@ export interface IDnsServerOptions {
|
||||
httpsPort: number;
|
||||
udpPort: number;
|
||||
dnssecZone: string;
|
||||
udpBindInterface?: string;
|
||||
httpsBindInterface?: string;
|
||||
// New options for independent manual socket control
|
||||
manualUdpMode?: boolean;
|
||||
manualHttpsMode?: boolean;
|
||||
}
|
||||
|
||||
export interface DnsAnswer {
|
||||
@ -31,18 +36,6 @@ interface DNSKEYData {
|
||||
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
|
||||
interface LetsEncryptOptions {
|
||||
email?: string;
|
||||
@ -60,6 +53,10 @@ export class DnsServer {
|
||||
private dnskeyRecord: DNSKEYData;
|
||||
private keyTag: number;
|
||||
|
||||
// Track if servers are initialized
|
||||
private udpServerInitialized: boolean = false;
|
||||
private httpsServerInitialized: boolean = false;
|
||||
|
||||
constructor(private options: IDnsServerOptions) {
|
||||
// Initialize DNSSEC
|
||||
this.dnsSec = new DnsSec({
|
||||
@ -70,13 +67,125 @@ export class DnsServer {
|
||||
});
|
||||
|
||||
// Generate DNSKEY and DS records
|
||||
const { dsRecord, dnskeyRecord } = this.dnsSec.getDsAndKeyPair();
|
||||
const { dnskeyRecord } = this.dnsSec.getDsAndKeyPair();
|
||||
|
||||
// Parse DNSKEY record into dns-packet format
|
||||
this.dnskeyRecord = this.parseDNSKEYRecord(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(
|
||||
domainPattern: string,
|
||||
recordTypes: string[],
|
||||
@ -183,7 +292,7 @@ export class DnsServer {
|
||||
const domain = auth.identifier.value;
|
||||
|
||||
// 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) {
|
||||
throw new Error(`No DNS-01 challenge found for ${domain}`);
|
||||
}
|
||||
@ -265,8 +374,10 @@ export class DnsServer {
|
||||
this.options.httpsCert = certificate;
|
||||
this.options.httpsKey = privateKey;
|
||||
|
||||
// Restart HTTPS server with new certificate
|
||||
await this.restartHttpsServer();
|
||||
// Restart HTTPS server with new certificate (only if not in manual HTTPS mode)
|
||||
if (!this.options.manualHttpsMode) {
|
||||
await this.restartHttpsServer();
|
||||
}
|
||||
|
||||
// Clean up challenge handlers
|
||||
for (const handler of challengeHandlers) {
|
||||
@ -334,10 +445,15 @@ export class DnsServer {
|
||||
this.handleHttpsRequest.bind(this)
|
||||
);
|
||||
|
||||
this.httpsServer.listen(this.options.httpsPort, () => {
|
||||
console.log(`HTTPS DNS server restarted on port ${this.options.httpsPort} with test certificate`);
|
||||
if (!this.options.manualHttpsMode) {
|
||||
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();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -351,10 +467,15 @@ export class DnsServer {
|
||||
this.handleHttpsRequest.bind(this)
|
||||
);
|
||||
|
||||
this.httpsServer.listen(this.options.httpsPort, () => {
|
||||
console.log(`HTTPS DNS server restarted on port ${this.options.httpsPort} with new certificate`);
|
||||
if (!this.options.manualHttpsMode) {
|
||||
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();
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error creating HTTPS server with new certificate:', err);
|
||||
reject(err);
|
||||
@ -386,6 +507,25 @@ export class DnsServer {
|
||||
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
|
||||
*/
|
||||
@ -683,45 +823,76 @@ export class DnsServer {
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
this.httpsServer = plugins.https.createServer(
|
||||
{
|
||||
key: this.options.httpsKey,
|
||||
cert: this.options.httpsCert,
|
||||
},
|
||||
this.handleHttpsRequest.bind(this)
|
||||
);
|
||||
|
||||
this.udpServer = plugins.dgram.createSocket('udp4');
|
||||
this.udpServer.on('message', (msg, rinfo) => {
|
||||
const request = dnsPacket.decode(msg);
|
||||
const response = this.processDnsRequest(request);
|
||||
const responseData = dnsPacket.encode(response);
|
||||
this.udpServer.send(responseData, rinfo.port, rinfo.address);
|
||||
});
|
||||
|
||||
this.udpServer.on('error', (err) => {
|
||||
console.error(`UDP Server error:\n${err.stack}`);
|
||||
this.udpServer.close();
|
||||
});
|
||||
|
||||
const udpListeningDeferred = plugins.smartpromise.defer<void>();
|
||||
const httpsListeningDeferred = plugins.smartpromise.defer<void>();
|
||||
|
||||
try {
|
||||
this.udpServer.bind(this.options.udpPort, '0.0.0.0', () => {
|
||||
console.log(`UDP DNS server running on port ${this.options.udpPort}`);
|
||||
udpListeningDeferred.resolve();
|
||||
});
|
||||
|
||||
this.httpsServer.listen(this.options.httpsPort, () => {
|
||||
console.log(`HTTPS DNS server running on port ${this.options.httpsPort}`);
|
||||
httpsListeningDeferred.resolve();
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error starting DNS server:', err);
|
||||
process.exit(1);
|
||||
// Initialize servers based on what's needed
|
||||
if (!this.options.manualUdpMode) {
|
||||
this.initializeUdpServer();
|
||||
}
|
||||
if (!this.options.manualHttpsMode) {
|
||||
this.initializeHttpsServer();
|
||||
}
|
||||
|
||||
// Handle different mode combinations
|
||||
const udpManual = this.options.manualUdpMode || false;
|
||||
const httpsManual = this.options.manualHttpsMode || false;
|
||||
|
||||
if (udpManual && httpsManual) {
|
||||
console.log('DNS server started in full manual mode - ready to accept connections');
|
||||
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>();
|
||||
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> {
|
||||
@ -755,6 +926,8 @@ export class DnsServer {
|
||||
}
|
||||
|
||||
await Promise.all([doneUdp.promise, doneHttps.promise]);
|
||||
this.udpServerInitialized = false;
|
||||
this.httpsServerInitialized = false;
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
@ -4,14 +4,16 @@ import dgram from 'dgram';
|
||||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import * as net from 'net';
|
||||
import * as path from 'path';
|
||||
|
||||
export {
|
||||
crypto,
|
||||
dgram,
|
||||
fs,
|
||||
http,
|
||||
https,
|
||||
dgram,
|
||||
net,
|
||||
path,
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user