feat(socket-handler): implement direct socket passing for DNS and email services

- Add socket-handler mode eliminating internal port binding for improved performance
- Add `dnsDomain` config option for automatic DNS-over-HTTPS (DoH) setup
- Add `useSocketHandler` flag to email config for direct socket processing
- Update SmartProxy route generation to support socket-handler actions
- Integrate smartdns with manual HTTPS mode for DoH without port binding
- Add automatic route creation for DNS paths when dnsDomain is configured
- Update documentation with socket-handler configuration and benefits
- Improve resource efficiency by eliminating internal port forwarding
This commit is contained in:
2025-05-29 16:26:19 +00:00
parent 6c8458f63c
commit b11fea7334
9 changed files with 687 additions and 540 deletions

View File

@ -52,7 +52,10 @@ export interface IDcRouterOptions {
};
/** DNS server configuration */
dnsServerConfig?: plugins.smartdns.IDnsServerOptions;
dnsServerConfig?: plugins.smartdns.dnsServerMod.IDnsServerOptions;
/** DNS domain for automatic DNS server setup with DoH */
dnsDomain?: string;
/** DNS challenge configuration for ACME (optional) */
dnsChallenge?: {
@ -81,7 +84,7 @@ export class DcRouter {
// Core services
public smartProxy?: plugins.smartproxy.SmartProxy;
public dnsServer?: plugins.smartdns.DnsServer;
public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer;
public emailServer?: UnifiedEmailServer;
@ -114,10 +117,13 @@ export class DcRouter {
}
}
// Set up DNS server if configured
if (this.options.dnsServerConfig) {
// Set up DNS server if configured by dnsDomain
if (this.options.dnsDomain) {
await this.setupDnsWithSocketHandler();
} else if (this.options.dnsServerConfig) {
// Legacy DNS server setup
const { records, ...dnsServerOptions } = this.options.dnsServerConfig as any;
this.dnsServer = new plugins.smartdns.DnsServer(dnsServerOptions);
this.dnsServer = new plugins.smartdns.dnsServerMod.DnsServer(dnsServerOptions);
// Register DNS record handlers if records provided
if (records && records.length > 0) {
@ -161,6 +167,13 @@ export class DcRouter {
routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy
}
// If DNS domain is configured, add DNS routes
if (this.options.dnsDomain) {
const dnsRoutes = this.generateDnsRoutes();
console.log(`DNS Routes for domain ${this.options.dnsDomain}:`, dnsRoutes);
routes = [...routes, ...dnsRoutes];
}
// Merge TLS/ACME configuration if provided at root level
if (this.options.tls && !acmeConfig) {
acmeConfig = {
@ -247,21 +260,8 @@ export class DcRouter {
private generateEmailRoutes(emailConfig: IUnifiedEmailServerOptions): plugins.smartproxy.IRouteConfig[] {
const emailRoutes: plugins.smartproxy.IRouteConfig[] = [];
// Get the custom port mapping if available, otherwise use defaults
const defaultPortMapping = {
25: 10025, // SMTP
587: 10587, // Submission
465: 10465 // SMTPS
};
// Use custom port mapping if provided, otherwise fall back to defaults
const portMapping = this.options.emailPortConfig?.portMapping || defaultPortMapping;
// Create routes for each email port
for (const port of emailConfig.ports) {
// Calculate the internal port using the mapping
const internalPort = portMapping[port] || port + 10000;
// Create a descriptive name for the route based on the port
let routeName = 'email-route';
let tlsMode = 'passthrough';
@ -285,7 +285,6 @@ export class DcRouter {
default:
routeName = `email-port-${port}-route`;
// For unknown ports, assume passthrough by default
tlsMode = 'passthrough';
// Check if we have specific settings for this port
@ -306,13 +305,27 @@ export class DcRouter {
break;
}
// Create the route configuration
const routeConfig: plugins.smartproxy.IRouteConfig = {
name: routeName,
match: {
ports: [port]
},
action: {
// Create action based on mode
let action: any;
if (emailConfig.useSocketHandler) {
// Socket-handler mode
action = {
type: 'socket-handler' as any,
socketHandler: this.createMailSocketHandler(port)
};
} else {
// Traditional forwarding mode
const defaultPortMapping = {
25: 10025, // SMTP
587: 10587, // Submission
465: 10465 // SMTPS
};
const portMapping = this.options.emailPortConfig?.portMapping || defaultPortMapping;
const internalPort = portMapping[port] || port + 10000;
action = {
type: 'forward',
target: {
host: 'localhost', // Forward to internal email server
@ -321,19 +334,28 @@ export class DcRouter {
tls: {
mode: tlsMode as any
}
}
};
};
}
// For TLS terminate mode, add certificate info
if (tlsMode === 'terminate') {
routeConfig.action.tls.certificate = 'auto';
if (tlsMode === 'terminate' && action.tls) {
action.tls.certificate = 'auto';
}
// Create the route configuration
const routeConfig: plugins.smartproxy.IRouteConfig = {
name: routeName,
match: {
ports: [port]
},
action: action
};
// Add the route to our list
emailRoutes.push(routeConfig);
}
// Add email routes if configured
// Add email domain-based routes if configured
if (emailConfig.routes) {
for (const route of emailConfig.routes) {
emailRoutes.push({
@ -358,6 +380,39 @@ export class DcRouter {
return emailRoutes;
}
/**
* Generate SmartProxy routes for DNS configuration
*/
private generateDnsRoutes(): plugins.smartproxy.IRouteConfig[] {
if (!this.options.dnsDomain) {
return [];
}
const dnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
// Create routes for DNS-over-HTTPS paths
const dohPaths = ['/dns-query', '/resolve'];
for (const path of dohPaths) {
const dohRoute: plugins.smartproxy.IRouteConfig = {
name: `dns-over-https-${path.replace('/', '')}`,
match: {
ports: [443], // HTTPS port for DoH
domains: [this.options.dnsDomain],
path: path
},
action: {
type: 'socket-handler' as any,
socketHandler: this.createDnsSocketHandler()
} as any
};
dnsRoutes.push(dohRoute);
}
return dnsRoutes;
}
/**
* Check if a domain matches a pattern (including wildcard support)
@ -663,6 +718,118 @@ export class DcRouter {
return value;
}
}
/**
* Set up DNS server with socket handler for DoH
*/
private async setupDnsWithSocketHandler(): Promise<void> {
if (!this.options.dnsDomain) {
throw new Error('dnsDomain is required for DNS socket handler setup');
}
logger.log('info', `Setting up DNS server with socket handler for domain: ${this.options.dnsDomain}`);
// Get VM IP address for UDP binding
const networkInterfaces = plugins.os.networkInterfaces();
let vmIpAddress = '0.0.0.0'; // Default to all interfaces
// Try to find the VM's internal IP address
for (const [name, interfaces] of Object.entries(networkInterfaces)) {
if (interfaces) {
for (const iface of interfaces) {
if (!iface.internal && iface.family === 'IPv4') {
vmIpAddress = iface.address;
break;
}
}
}
}
// Create DNS server instance with manual HTTPS mode
this.dnsServer = new plugins.smartdns.dnsServerMod.DnsServer({
udpPort: 53,
udpBindInterface: vmIpAddress,
httpsPort: 443, // Required but won't bind due to manual mode
manualHttpsMode: true, // Enable manual HTTPS socket handling
dnssecZone: this.options.dnsDomain,
// For now, use self-signed cert until we integrate with Let's Encrypt
httpsKey: '',
httpsCert: ''
});
// Start the DNS server (UDP only)
await this.dnsServer.start();
logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`);
}
/**
* Create DNS socket handler for DoH
*/
private createDnsSocketHandler(): (socket: plugins.net.Socket) => Promise<void> {
return async (socket: plugins.net.Socket) => {
if (!this.dnsServer) {
logger.log('error', 'DNS socket handler called but DNS server not initialized');
socket.end();
return;
}
logger.log('debug', 'DNS socket handler: passing socket to DnsServer');
try {
// Use the built-in socket handler from smartdns
// This handles HTTP/2, DoH protocol, etc.
await (this.dnsServer as any).handleHttpsSocket(socket);
} catch (error) {
logger.log('error', `DNS socket handler error: ${error.message}`);
socket.destroy();
}
};
}
/**
* Create mail socket handler for email traffic
*/
private createMailSocketHandler(port: number): (socket: plugins.net.Socket) => Promise<void> {
return async (socket: plugins.net.Socket) => {
if (!this.emailServer) {
logger.log('error', 'Mail socket handler called but email server not initialized');
socket.end();
return;
}
logger.log('debug', `Mail socket handler: handling connection for port ${port}`);
try {
// Port 465 requires immediate TLS
if (port === 465) {
// Wrap the socket in TLS
const tlsOptions = {
isServer: true,
key: this.options.tls?.keyPath ? plugins.fs.readFileSync(this.options.tls.keyPath, 'utf8') : undefined,
cert: this.options.tls?.certPath ? plugins.fs.readFileSync(this.options.tls.certPath, 'utf8') : undefined
};
const tlsSocket = new plugins.tls.TLSSocket(socket, tlsOptions);
tlsSocket.on('secure', () => {
// Pass the secure socket to the email server
this.emailServer!.handleSocket(tlsSocket, port);
});
tlsSocket.on('error', (err) => {
logger.log('error', `TLS handshake error on port ${port}: ${err.message}`);
socket.destroy();
});
} else {
// For ports 25 and 587, pass raw socket (STARTTLS handled by email server)
await this.emailServer.handleSocket(socket, port);
}
} catch (error) {
logger.log('error', `Mail socket handler error on port ${port}: ${error.message}`);
socket.destroy();
}
};
}
}
// Re-export email server types for convenience

View File

@ -49,6 +49,7 @@ export interface IUnifiedEmailServerOptions {
domains: string[]; // Domains to handle email for
banner?: string;
debug?: boolean;
useSocketHandler?: boolean; // Use socket-handler mode instead of port listening
// Authentication options
auth?: {
@ -336,6 +337,13 @@ export class UnifiedEmailServer extends EventEmitter {
logger.log('info', 'Automatic DKIM configuration completed');
}
// Skip server creation in socket-handler mode
if (this.options.useSocketHandler) {
logger.log('info', 'UnifiedEmailServer started in socket-handler mode (no port listening)');
this.emit('started');
return;
}
// Ensure we have the necessary TLS options
const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath;
@ -446,6 +454,54 @@ export class UnifiedEmailServer extends EventEmitter {
}
}
/**
* Handle a socket from smartproxy in socket-handler mode
* @param socket The socket to handle
* @param port The port this connection is for (25, 587, 465)
*/
public async handleSocket(socket: plugins.net.Socket | plugins.tls.TLSSocket, port: number): Promise<void> {
if (!this.options.useSocketHandler) {
logger.log('error', 'handleSocket called but useSocketHandler is not enabled');
socket.destroy();
return;
}
logger.log('info', `Handling socket for port ${port}`);
// Create a temporary SMTP server instance for this connection
// We need a full server instance because the SMTP protocol handler needs all components
const smtpServerOptions = {
port,
hostname: this.options.hostname,
key: this.options.tls?.keyPath ? plugins.fs.readFileSync(this.options.tls.keyPath, 'utf8') : undefined,
cert: this.options.tls?.certPath ? plugins.fs.readFileSync(this.options.tls.certPath, 'utf8') : undefined
};
// Create the SMTP server instance
const smtpServer = createSmtpServer(this, smtpServerOptions);
// Get the connection manager from the server
const connectionManager = (smtpServer as any).connectionManager;
if (!connectionManager) {
logger.log('error', 'Could not get connection manager from SMTP server');
socket.destroy();
return;
}
// Determine if this is a secure connection
// Port 465 uses implicit TLS, so the socket is already secure
const isSecure = port === 465 || socket instanceof plugins.tls.TLSSocket;
// Pass the socket to the connection manager
try {
await connectionManager.handleConnection(socket, isSecure);
} catch (error) {
logger.log('error', `Error handling socket connection: ${error.message}`);
socket.destroy();
}
}
/**
* Stop the unified email server
*/

View File

@ -4,6 +4,7 @@ import * as fs from 'fs';
import * as crypto from 'crypto';
import * as http from 'http';
import * as net from 'net';
import * as os from 'os';
import * as path from 'path';
import * as tls from 'tls';
import * as util from 'util';
@ -14,6 +15,7 @@ export {
crypto,
http,
net,
os,
path,
tls,
util,