785 lines
26 KiB
TypeScript
785 lines
26 KiB
TypeScript
import * as plugins from '../../plugins.js';
|
|
|
|
// Importing required components
|
|
import { ConnectionManager } from './connection-manager.js';
|
|
import { SecurityManager } from './security-manager.js';
|
|
import { TlsManager } from './tls-manager.js';
|
|
import { NetworkProxyBridge } from './network-proxy-bridge.js';
|
|
import { TimeoutManager } from './timeout-manager.js';
|
|
import { PortManager } from './port-manager.js';
|
|
import { RouteManager } from './route-manager.js';
|
|
import { RouteConnectionHandler } from './route-connection-handler.js';
|
|
import { NFTablesManager } from './nftables-manager.js';
|
|
|
|
// Certificate manager
|
|
import { SmartCertManager, type ICertStatus } from './certificate-manager.js';
|
|
|
|
// Import types and utilities
|
|
import type {
|
|
ISmartProxyOptions
|
|
} from './models/interfaces.js';
|
|
import type { IRouteConfig } from './models/route-types.js';
|
|
|
|
/**
|
|
* SmartProxy - Pure route-based API
|
|
*
|
|
* SmartProxy is a unified proxy system that works with routes to define connection handling behavior.
|
|
* Each route contains matching criteria (ports, domains, etc.) and an action to take (forward, redirect, block).
|
|
*
|
|
* Configuration is provided through a set of routes, with each route defining:
|
|
* - What to match (ports, domains, paths, client IPs)
|
|
* - What to do with matching traffic (forward, redirect, block)
|
|
* - How to handle TLS (passthrough, terminate, terminate-and-reencrypt)
|
|
* - Security settings (IP restrictions, connection limits)
|
|
* - Advanced options (timeout, headers, etc.)
|
|
*/
|
|
export class SmartProxy extends plugins.EventEmitter {
|
|
// Port manager handles dynamic listener management
|
|
private portManager: PortManager;
|
|
private connectionLogger: NodeJS.Timeout | null = null;
|
|
private isShuttingDown: boolean = false;
|
|
|
|
// Component managers
|
|
private connectionManager: ConnectionManager;
|
|
private securityManager: SecurityManager;
|
|
private tlsManager: TlsManager;
|
|
private networkProxyBridge: NetworkProxyBridge;
|
|
private timeoutManager: TimeoutManager;
|
|
public routeManager: RouteManager; // Made public for route management
|
|
private routeConnectionHandler: RouteConnectionHandler;
|
|
private nftablesManager: NFTablesManager;
|
|
|
|
// Certificate manager for ACME and static certificates
|
|
private certManager: SmartCertManager | null = null;
|
|
|
|
/**
|
|
* Constructor for SmartProxy
|
|
*
|
|
* @param settingsArg Configuration options containing routes and other settings
|
|
* Routes define how traffic is matched and handled, with each route having:
|
|
* - match: criteria for matching traffic (ports, domains, paths, IPs)
|
|
* - action: what to do with matched traffic (forward, redirect, block)
|
|
*
|
|
* Example:
|
|
* ```ts
|
|
* const proxy = new SmartProxy({
|
|
* routes: [
|
|
* {
|
|
* match: {
|
|
* ports: 443,
|
|
* domains: ['example.com', '*.example.com']
|
|
* },
|
|
* action: {
|
|
* type: 'forward',
|
|
* target: { host: '10.0.0.1', port: 8443 },
|
|
* tls: { mode: 'passthrough' }
|
|
* }
|
|
* }
|
|
* ],
|
|
* defaults: {
|
|
* target: { host: 'localhost', port: 8080 },
|
|
* security: { ipAllowList: ['*'] }
|
|
* }
|
|
* });
|
|
* ```
|
|
*/
|
|
constructor(settingsArg: ISmartProxyOptions) {
|
|
super();
|
|
|
|
// Set reasonable defaults for all settings
|
|
this.settings = {
|
|
...settingsArg,
|
|
initialDataTimeout: settingsArg.initialDataTimeout || 120000,
|
|
socketTimeout: settingsArg.socketTimeout || 3600000,
|
|
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000,
|
|
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 86400000,
|
|
inactivityTimeout: settingsArg.inactivityTimeout || 14400000,
|
|
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000,
|
|
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
|
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
|
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000,
|
|
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024,
|
|
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
|
enableKeepAliveProbes:
|
|
settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true,
|
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
|
allowSessionTicket:
|
|
settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true,
|
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
|
|
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
|
|
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended',
|
|
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
|
|
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000,
|
|
networkProxyPort: settingsArg.networkProxyPort || 8443,
|
|
};
|
|
|
|
// Normalize ACME options if provided (support both email and accountEmail)
|
|
if (this.settings.acme) {
|
|
// Support both 'email' and 'accountEmail' fields
|
|
if (this.settings.acme.accountEmail && !this.settings.acme.email) {
|
|
this.settings.acme.email = this.settings.acme.accountEmail;
|
|
}
|
|
|
|
// Set reasonable defaults for commonly used fields
|
|
this.settings.acme = {
|
|
enabled: this.settings.acme.enabled !== false, // Enable by default if acme object exists
|
|
port: this.settings.acme.port || 80,
|
|
email: this.settings.acme.email,
|
|
useProduction: this.settings.acme.useProduction || false,
|
|
renewThresholdDays: this.settings.acme.renewThresholdDays || 30,
|
|
autoRenew: this.settings.acme.autoRenew !== false, // Enable by default
|
|
certificateStore: this.settings.acme.certificateStore || './certs',
|
|
skipConfiguredCerts: this.settings.acme.skipConfiguredCerts || false,
|
|
renewCheckIntervalHours: this.settings.acme.renewCheckIntervalHours || 24,
|
|
routeForwards: this.settings.acme.routeForwards || [],
|
|
...this.settings.acme // Preserve any additional fields
|
|
};
|
|
}
|
|
|
|
// Initialize component managers
|
|
this.timeoutManager = new TimeoutManager(this.settings);
|
|
this.securityManager = new SecurityManager(this.settings);
|
|
this.connectionManager = new ConnectionManager(
|
|
this.settings,
|
|
this.securityManager,
|
|
this.timeoutManager
|
|
);
|
|
|
|
// Create the route manager
|
|
this.routeManager = new RouteManager(this.settings);
|
|
|
|
|
|
// Create other required components
|
|
this.tlsManager = new TlsManager(this.settings);
|
|
this.networkProxyBridge = new NetworkProxyBridge(this.settings);
|
|
|
|
// Initialize connection handler with route support
|
|
this.routeConnectionHandler = new RouteConnectionHandler(
|
|
this.settings,
|
|
this.connectionManager,
|
|
this.securityManager,
|
|
this.tlsManager,
|
|
this.networkProxyBridge,
|
|
this.timeoutManager,
|
|
this.routeManager
|
|
);
|
|
|
|
// Initialize port manager
|
|
this.portManager = new PortManager(this.settings, this.routeConnectionHandler);
|
|
|
|
// Initialize NFTablesManager
|
|
this.nftablesManager = new NFTablesManager(this.settings);
|
|
}
|
|
|
|
/**
|
|
* The settings for the SmartProxy
|
|
*/
|
|
public settings: ISmartProxyOptions;
|
|
|
|
/**
|
|
* Initialize certificate manager
|
|
*/
|
|
private async initializeCertificateManager(): Promise<void> {
|
|
// Extract global ACME options if any routes use auto certificates
|
|
const autoRoutes = this.settings.routes.filter(r =>
|
|
r.action.tls?.certificate === 'auto'
|
|
);
|
|
|
|
if (autoRoutes.length === 0 && !this.hasStaticCertRoutes()) {
|
|
console.log('No routes require certificate management');
|
|
return;
|
|
}
|
|
|
|
// Prepare ACME options with priority:
|
|
// 1. Use top-level ACME config if available
|
|
// 2. Fall back to first auto route's ACME config
|
|
// 3. Otherwise use undefined
|
|
let acmeOptions: { email?: string; useProduction?: boolean; port?: number } | undefined;
|
|
|
|
if (this.settings.acme?.email) {
|
|
// Use top-level ACME config
|
|
acmeOptions = {
|
|
email: this.settings.acme.email,
|
|
useProduction: this.settings.acme.useProduction || false,
|
|
port: this.settings.acme.port || 80
|
|
};
|
|
console.log(`Using top-level ACME configuration with email: ${acmeOptions.email}`);
|
|
} else if (autoRoutes.length > 0) {
|
|
// Check for route-level ACME config
|
|
const routeWithAcme = autoRoutes.find(r => r.action.tls?.acme?.email);
|
|
if (routeWithAcme?.action.tls?.acme) {
|
|
const routeAcme = routeWithAcme.action.tls.acme;
|
|
acmeOptions = {
|
|
email: routeAcme.email,
|
|
useProduction: routeAcme.useProduction || false,
|
|
port: routeAcme.challengePort || 80
|
|
};
|
|
console.log(`Using route-level ACME configuration from route '${routeWithAcme.name}' with email: ${acmeOptions.email}`);
|
|
}
|
|
}
|
|
|
|
// Validate we have required configuration
|
|
if (autoRoutes.length > 0 && !acmeOptions?.email) {
|
|
throw new Error(
|
|
'ACME email is required for automatic certificate provisioning. ' +
|
|
'Please provide email in either:\n' +
|
|
'1. Top-level "acme" configuration\n' +
|
|
'2. Individual route\'s "tls.acme" configuration'
|
|
);
|
|
}
|
|
|
|
this.certManager = new SmartCertManager(
|
|
this.settings.routes,
|
|
this.settings.acme?.certificateStore || './certs',
|
|
acmeOptions
|
|
);
|
|
|
|
// Pass down the global ACME config to the cert manager
|
|
if (this.settings.acme) {
|
|
this.certManager.setGlobalAcmeDefaults(this.settings.acme);
|
|
}
|
|
|
|
// Connect with NetworkProxy
|
|
if (this.networkProxyBridge.getNetworkProxy()) {
|
|
this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
|
|
}
|
|
|
|
// Set route update callback for ACME challenges
|
|
this.certManager.setUpdateRoutesCallback(async (routes) => {
|
|
await this.updateRoutes(routes);
|
|
});
|
|
|
|
await this.certManager.initialize();
|
|
}
|
|
|
|
/**
|
|
* Check if we have routes with static certificates
|
|
*/
|
|
private hasStaticCertRoutes(): boolean {
|
|
return this.settings.routes.some(r =>
|
|
r.action.tls?.certificate &&
|
|
r.action.tls.certificate !== 'auto'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Start the proxy server with support for both configuration types
|
|
*/
|
|
public async start() {
|
|
// Don't start if already shutting down
|
|
if (this.isShuttingDown) {
|
|
console.log("Cannot start SmartProxy while it's shutting down");
|
|
return;
|
|
}
|
|
|
|
// Initialize certificate manager before starting servers
|
|
await this.initializeCertificateManager();
|
|
|
|
// Initialize and start NetworkProxy if needed
|
|
if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
|
|
await this.networkProxyBridge.initialize();
|
|
|
|
// Connect NetworkProxy with certificate manager
|
|
if (this.certManager) {
|
|
this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
|
|
}
|
|
|
|
await this.networkProxyBridge.start();
|
|
}
|
|
|
|
// Validate the route configuration
|
|
const configWarnings = this.routeManager.validateConfiguration();
|
|
|
|
// Also validate ACME configuration
|
|
const acmeWarnings = this.validateAcmeConfiguration();
|
|
const allWarnings = [...configWarnings, ...acmeWarnings];
|
|
|
|
if (allWarnings.length > 0) {
|
|
console.log("Configuration warnings:");
|
|
for (const warning of allWarnings) {
|
|
console.log(` - ${warning}`);
|
|
}
|
|
}
|
|
|
|
// Get listening ports from RouteManager
|
|
const listeningPorts = this.routeManager.getListeningPorts();
|
|
|
|
// Provision NFTables rules for routes that use NFTables
|
|
for (const route of this.settings.routes) {
|
|
if (route.action.forwardingEngine === 'nftables') {
|
|
await this.nftablesManager.provisionRoute(route);
|
|
}
|
|
}
|
|
|
|
// Start port listeners using the PortManager
|
|
await this.portManager.addPorts(listeningPorts);
|
|
|
|
// Set up periodic connection logging and inactivity checks
|
|
this.connectionLogger = setInterval(() => {
|
|
// Immediately return if shutting down
|
|
if (this.isShuttingDown) return;
|
|
|
|
// Perform inactivity check
|
|
this.connectionManager.performInactivityCheck();
|
|
|
|
// Log connection statistics
|
|
const now = Date.now();
|
|
let maxIncoming = 0;
|
|
let maxOutgoing = 0;
|
|
let tlsConnections = 0;
|
|
let nonTlsConnections = 0;
|
|
let completedTlsHandshakes = 0;
|
|
let pendingTlsHandshakes = 0;
|
|
let keepAliveConnections = 0;
|
|
let networkProxyConnections = 0;
|
|
|
|
// Get connection records for analysis
|
|
const connectionRecords = this.connectionManager.getConnections();
|
|
|
|
// Analyze active connections
|
|
for (const record of connectionRecords.values()) {
|
|
// Track connection stats
|
|
if (record.isTLS) {
|
|
tlsConnections++;
|
|
if (record.tlsHandshakeComplete) {
|
|
completedTlsHandshakes++;
|
|
} else {
|
|
pendingTlsHandshakes++;
|
|
}
|
|
} else {
|
|
nonTlsConnections++;
|
|
}
|
|
|
|
if (record.hasKeepAlive) {
|
|
keepAliveConnections++;
|
|
}
|
|
|
|
if (record.usingNetworkProxy) {
|
|
networkProxyConnections++;
|
|
}
|
|
|
|
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
|
|
if (record.outgoingStartTime) {
|
|
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
|
|
}
|
|
}
|
|
|
|
// Get termination stats
|
|
const terminationStats = this.connectionManager.getTerminationStats();
|
|
|
|
// Log detailed stats
|
|
console.log(
|
|
`Active connections: ${connectionRecords.size}. ` +
|
|
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
|
|
`Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` +
|
|
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` +
|
|
`Termination stats: ${JSON.stringify({
|
|
IN: terminationStats.incoming,
|
|
OUT: terminationStats.outgoing,
|
|
})}`
|
|
);
|
|
}, this.settings.inactivityCheckInterval || 60000);
|
|
|
|
// Make sure the interval doesn't keep the process alive
|
|
if (this.connectionLogger.unref) {
|
|
this.connectionLogger.unref();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract domain configurations from routes for certificate provisioning
|
|
*
|
|
* Note: This method has been removed as we now work directly with routes
|
|
*/
|
|
|
|
/**
|
|
* Stop the proxy server
|
|
*/
|
|
public async stop() {
|
|
console.log('SmartProxy shutting down...');
|
|
this.isShuttingDown = true;
|
|
this.portManager.setShuttingDown(true);
|
|
|
|
// Stop certificate manager
|
|
if (this.certManager) {
|
|
await this.certManager.stop();
|
|
console.log('Certificate manager stopped');
|
|
}
|
|
|
|
// Stop NFTablesManager
|
|
await this.nftablesManager.stop();
|
|
console.log('NFTablesManager stopped');
|
|
|
|
// Stop the connection logger
|
|
if (this.connectionLogger) {
|
|
clearInterval(this.connectionLogger);
|
|
this.connectionLogger = null;
|
|
}
|
|
|
|
// Stop all port listeners
|
|
await this.portManager.closeAll();
|
|
console.log('All servers closed. Cleaning up active connections...');
|
|
|
|
// Clean up all active connections
|
|
this.connectionManager.clearConnections();
|
|
|
|
// Stop NetworkProxy
|
|
await this.networkProxyBridge.stop();
|
|
|
|
|
|
console.log('SmartProxy shutdown complete.');
|
|
}
|
|
|
|
/**
|
|
* Updates the domain configurations for the proxy
|
|
*
|
|
* Note: This legacy method has been removed. Use updateRoutes instead.
|
|
*/
|
|
public async updateDomainConfigs(): Promise<void> {
|
|
console.warn('Method updateDomainConfigs() is deprecated. Use updateRoutes() instead.');
|
|
throw new Error('updateDomainConfigs() is deprecated - use updateRoutes() instead');
|
|
}
|
|
|
|
/**
|
|
* Update routes with new configuration
|
|
*
|
|
* This method replaces the current route configuration with the provided routes.
|
|
* It also provisions certificates for routes that require TLS termination and have
|
|
* `certificate: 'auto'` set in their TLS configuration.
|
|
*
|
|
* @param newRoutes Array of route configurations to use
|
|
*
|
|
* Example:
|
|
* ```ts
|
|
* proxy.updateRoutes([
|
|
* {
|
|
* match: { ports: 443, domains: 'secure.example.com' },
|
|
* action: {
|
|
* type: 'forward',
|
|
* target: { host: '10.0.0.1', port: 8443 },
|
|
* tls: { mode: 'terminate', certificate: 'auto' }
|
|
* }
|
|
* }
|
|
* ]);
|
|
* ```
|
|
*/
|
|
public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> {
|
|
console.log(`Updating routes (${newRoutes.length} routes)`);
|
|
|
|
// Get existing routes that use NFTables
|
|
const oldNfTablesRoutes = this.settings.routes.filter(
|
|
r => r.action.forwardingEngine === 'nftables'
|
|
);
|
|
|
|
// Get new routes that use NFTables
|
|
const newNfTablesRoutes = newRoutes.filter(
|
|
r => r.action.forwardingEngine === 'nftables'
|
|
);
|
|
|
|
// Find routes to remove, update, or add
|
|
for (const oldRoute of oldNfTablesRoutes) {
|
|
const newRoute = newNfTablesRoutes.find(r => r.name === oldRoute.name);
|
|
|
|
if (!newRoute) {
|
|
// Route was removed
|
|
await this.nftablesManager.deprovisionRoute(oldRoute);
|
|
} else {
|
|
// Route was updated
|
|
await this.nftablesManager.updateRoute(oldRoute, newRoute);
|
|
}
|
|
}
|
|
|
|
// Find new routes to add
|
|
for (const newRoute of newNfTablesRoutes) {
|
|
const oldRoute = oldNfTablesRoutes.find(r => r.name === newRoute.name);
|
|
|
|
if (!oldRoute) {
|
|
// New route
|
|
await this.nftablesManager.provisionRoute(newRoute);
|
|
}
|
|
}
|
|
|
|
// Update routes in RouteManager
|
|
this.routeManager.updateRoutes(newRoutes);
|
|
|
|
// Get the new set of required ports
|
|
const requiredPorts = this.routeManager.getListeningPorts();
|
|
|
|
// Update port listeners to match the new configuration
|
|
await this.portManager.updatePorts(requiredPorts);
|
|
|
|
// Update settings with the new routes
|
|
this.settings.routes = newRoutes;
|
|
|
|
// If NetworkProxy is initialized, resync the configurations
|
|
if (this.networkProxyBridge.getNetworkProxy()) {
|
|
await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes);
|
|
}
|
|
|
|
// Update certificate manager with new routes
|
|
if (this.certManager) {
|
|
await this.certManager.stop();
|
|
|
|
this.certManager = new SmartCertManager(
|
|
newRoutes,
|
|
'./certs',
|
|
this.certManager.getAcmeOptions()
|
|
);
|
|
|
|
if (this.networkProxyBridge.getNetworkProxy()) {
|
|
this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
|
|
}
|
|
|
|
await this.certManager.initialize();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Manually provision a certificate for a route
|
|
*/
|
|
public async provisionCertificate(routeName: string): Promise<void> {
|
|
if (!this.certManager) {
|
|
throw new Error('Certificate manager not initialized');
|
|
}
|
|
|
|
const route = this.settings.routes.find(r => r.name === routeName);
|
|
if (!route) {
|
|
throw new Error(`Route ${routeName} not found`);
|
|
}
|
|
|
|
await this.certManager.provisionCertificate(route);
|
|
}
|
|
|
|
/**
|
|
* Force renewal of a certificate
|
|
*/
|
|
public async renewCertificate(routeName: string): Promise<void> {
|
|
if (!this.certManager) {
|
|
throw new Error('Certificate manager not initialized');
|
|
}
|
|
|
|
await this.certManager.renewCertificate(routeName);
|
|
}
|
|
|
|
/**
|
|
* Get certificate status for a route
|
|
*/
|
|
public getCertificateStatus(routeName: string): ICertStatus | undefined {
|
|
if (!this.certManager) {
|
|
return undefined;
|
|
}
|
|
|
|
return this.certManager.getCertificateStatus(routeName);
|
|
}
|
|
|
|
/**
|
|
* Validates if a domain name is valid for certificate issuance
|
|
*/
|
|
private isValidDomain(domain: string): boolean {
|
|
// Very basic domain validation
|
|
if (!domain || domain.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check for wildcard domains (they can't get ACME certs)
|
|
if (domain.includes('*')) {
|
|
console.log(`Wildcard domains like "${domain}" are not supported for ACME certificates`);
|
|
return false;
|
|
}
|
|
|
|
// Check if domain has at least one dot and no invalid characters
|
|
const validDomainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
if (!validDomainRegex.test(domain)) {
|
|
console.log(`Domain "${domain}" has invalid format`);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Add a new listening port without changing the route configuration
|
|
*
|
|
* This allows you to add a port listener without updating routes.
|
|
* Useful for preparing to listen on a port before adding routes for it.
|
|
*
|
|
* @param port The port to start listening on
|
|
* @returns Promise that resolves when the port is listening
|
|
*/
|
|
public async addListeningPort(port: number): Promise<void> {
|
|
return this.portManager.addPort(port);
|
|
}
|
|
|
|
/**
|
|
* Stop listening on a specific port without changing the route configuration
|
|
*
|
|
* This allows you to stop a port listener without updating routes.
|
|
* Useful for temporary maintenance or port changes.
|
|
*
|
|
* @param port The port to stop listening on
|
|
* @returns Promise that resolves when the port is closed
|
|
*/
|
|
public async removeListeningPort(port: number): Promise<void> {
|
|
return this.portManager.removePort(port);
|
|
}
|
|
|
|
/**
|
|
* Get a list of all ports currently being listened on
|
|
*
|
|
* @returns Array of port numbers
|
|
*/
|
|
public getListeningPorts(): number[] {
|
|
return this.portManager.getListeningPorts();
|
|
}
|
|
|
|
/**
|
|
* Get statistics about current connections
|
|
*/
|
|
public getStatistics(): any {
|
|
const connectionRecords = this.connectionManager.getConnections();
|
|
const terminationStats = this.connectionManager.getTerminationStats();
|
|
|
|
let tlsConnections = 0;
|
|
let nonTlsConnections = 0;
|
|
let keepAliveConnections = 0;
|
|
let networkProxyConnections = 0;
|
|
|
|
// Analyze active connections
|
|
for (const record of connectionRecords.values()) {
|
|
if (record.isTLS) tlsConnections++;
|
|
else nonTlsConnections++;
|
|
if (record.hasKeepAlive) keepAliveConnections++;
|
|
if (record.usingNetworkProxy) networkProxyConnections++;
|
|
}
|
|
|
|
return {
|
|
activeConnections: connectionRecords.size,
|
|
tlsConnections,
|
|
nonTlsConnections,
|
|
keepAliveConnections,
|
|
networkProxyConnections,
|
|
terminationStats,
|
|
acmeEnabled: !!this.certManager,
|
|
port80HandlerPort: this.certManager ? 80 : null,
|
|
routes: this.routeManager.getListeningPorts().length,
|
|
listeningPorts: this.portManager.getListeningPorts(),
|
|
activePorts: this.portManager.getListeningPorts().length
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get a list of eligible domains for ACME certificates
|
|
*/
|
|
public getEligibleDomainsForCertificates(): string[] {
|
|
const domains: string[] = [];
|
|
|
|
// Get domains from routes
|
|
const routes = this.settings.routes || [];
|
|
|
|
for (const route of routes) {
|
|
if (!route.match.domains) continue;
|
|
|
|
// Skip routes without TLS termination or auto certificates
|
|
if (route.action.type !== 'forward' ||
|
|
!route.action.tls ||
|
|
route.action.tls.mode === 'passthrough' ||
|
|
route.action.tls.certificate !== 'auto') continue;
|
|
|
|
const routeDomains = Array.isArray(route.match.domains)
|
|
? route.match.domains
|
|
: [route.match.domains];
|
|
|
|
// Skip domains that can't be used with ACME
|
|
const eligibleDomains = routeDomains.filter(domain =>
|
|
!domain.includes('*') && this.isValidDomain(domain)
|
|
);
|
|
|
|
domains.push(...eligibleDomains);
|
|
}
|
|
|
|
// Legacy mode is no longer supported
|
|
|
|
return domains;
|
|
}
|
|
|
|
/**
|
|
* Get NFTables status
|
|
*/
|
|
public async getNfTablesStatus(): Promise<Record<string, any>> {
|
|
return this.nftablesManager.getStatus();
|
|
}
|
|
|
|
/**
|
|
* Validate ACME configuration
|
|
*/
|
|
private validateAcmeConfiguration(): string[] {
|
|
const warnings: string[] = [];
|
|
|
|
// Check for routes with certificate: 'auto'
|
|
const autoRoutes = this.settings.routes.filter(r =>
|
|
r.action.tls?.certificate === 'auto'
|
|
);
|
|
|
|
if (autoRoutes.length === 0) {
|
|
return warnings;
|
|
}
|
|
|
|
// Check if we have ACME email configuration
|
|
const hasTopLevelEmail = this.settings.acme?.email;
|
|
const routesWithEmail = autoRoutes.filter(r => r.action.tls?.acme?.email);
|
|
|
|
if (!hasTopLevelEmail && routesWithEmail.length === 0) {
|
|
warnings.push(
|
|
'Routes with certificate: "auto" require ACME email configuration. ' +
|
|
'Add email to either top-level "acme" config or individual route\'s "tls.acme" config.'
|
|
);
|
|
}
|
|
|
|
// Check for port 80 availability for challenges
|
|
if (autoRoutes.length > 0) {
|
|
const challengePort = this.settings.acme?.port || 80;
|
|
const portsInUse = this.routeManager.getListeningPorts();
|
|
|
|
if (!portsInUse.includes(challengePort)) {
|
|
warnings.push(
|
|
`Port ${challengePort} is not configured for any routes but is needed for ACME challenges. ` +
|
|
`Add a route listening on port ${challengePort} or ensure it's accessible for HTTP-01 challenges.`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Check for mismatched environments
|
|
if (this.settings.acme?.useProduction) {
|
|
const stagingRoutes = autoRoutes.filter(r =>
|
|
r.action.tls?.acme?.useProduction === false
|
|
);
|
|
if (stagingRoutes.length > 0) {
|
|
warnings.push(
|
|
'Top-level ACME uses production but some routes use staging. ' +
|
|
'Consider aligning environments to avoid certificate issues.'
|
|
);
|
|
}
|
|
}
|
|
|
|
// Check for wildcard domains with auto certificates
|
|
for (const route of autoRoutes) {
|
|
const domains = Array.isArray(route.match.domains)
|
|
? route.match.domains
|
|
: [route.match.domains];
|
|
|
|
const wildcardDomains = domains.filter(d => d?.includes('*'));
|
|
if (wildcardDomains.length > 0) {
|
|
warnings.push(
|
|
`Route "${route.name}" has wildcard domain(s) ${wildcardDomains.join(', ')} ` +
|
|
'with certificate: "auto". Wildcard certificates require DNS-01 challenges, ' +
|
|
'which are not currently supported. Use static certificates instead.'
|
|
);
|
|
}
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
} |