fix(logger): Replace raw console logging calls with structured logger usage across certificate management, connection handling, and route processing for improved observability.

This commit is contained in:
Philipp Kunz 2025-05-19 23:37:11 +00:00
parent e61766959f
commit c9abdea556
6 changed files with 456 additions and 240 deletions

View File

@ -1,5 +1,13 @@
# Changelog # Changelog
## 2025-05-19 - 19.3.11 - fix(logger)
Replace raw console logging calls with structured logger usage across certificate management, connection handling, and route processing for improved observability.
- Replaced console.log, console.warn, and console.error in SmartCertManager with logger.log for more consistent logging.
- Updated ConnectionManager and RouteConnectionHandler to log detailed connection events using a structured logger.
- Enhanced logging statements with contextual metadata such as connection IDs, remote IPs, target information, and component identifiers.
- Standardized log output across proxy modules to aid in debugging and monitoring.
## 2025-05-19 - 19.3.10 - fix(certificate-manager, smart-proxy) ## 2025-05-19 - 19.3.10 - fix(certificate-manager, smart-proxy)
Fix race condition in ACME certificate provisioning and refactor certificate manager initialization to defer provisioning until after port listeners are active Fix race condition in ACME certificate provisioning and refactor certificate manager initialization to defer provisioning until after port listeners are active

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartproxy', name: '@push.rocks/smartproxy',
version: '19.3.10', version: '19.3.11',
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.' description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
} }

View File

@ -4,6 +4,7 @@ import type { IRouteConfig, IRouteTls } from './models/route-types.js';
import type { IAcmeOptions } from './models/interfaces.js'; import type { IAcmeOptions } from './models/interfaces.js';
import { CertStore } from './cert-store.js'; import { CertStore } from './cert-store.js';
import type { AcmeStateManager } from './acme-state-manager.js'; import type { AcmeStateManager } from './acme-state-manager.js';
import { logger } from '../../core/utils/logger.js';
export interface ICertStatus { export interface ICertStatus {
domain: string; domain: string;
@ -125,16 +126,16 @@ export class SmartCertManager {
// Add challenge route once at initialization if not already active // Add challenge route once at initialization if not already active
if (!this.challengeRouteActive) { if (!this.challengeRouteActive) {
console.log('Adding ACME challenge route during initialization'); logger.log('info', 'Adding ACME challenge route during initialization', { component: 'certificate-manager' });
await this.addChallengeRoute(); await this.addChallengeRoute();
} else { } else {
console.log('Challenge route already active from previous instance'); logger.log('info', 'Challenge route already active from previous instance', { component: 'certificate-manager' });
} }
} }
// Skip automatic certificate provisioning during initialization // Skip automatic certificate provisioning during initialization
// This will be called later after ports are listening // This will be called later after ports are listening
console.log('Certificate manager initialized. Deferring certificate provisioning until after ports are listening.'); logger.log('info', 'Certificate manager initialized. Deferring certificate provisioning until after ports are listening.', { component: 'certificate-manager' });
// Start renewal timer // Start renewal timer
this.startRenewalTimer(); this.startRenewalTimer();
@ -157,7 +158,7 @@ export class SmartCertManager {
try { try {
await this.provisionCertificate(route, true); // Allow concurrent since we're managing it here await this.provisionCertificate(route, true); // Allow concurrent since we're managing it here
} catch (error) { } catch (error) {
console.error(`Failed to provision certificate for route ${route.name}: ${error}`); logger.log('error', `Failed to provision certificate for route ${route.name}`, { routeName: route.name, error, component: 'certificate-manager' });
} }
} }
} finally { } finally {
@ -176,13 +177,13 @@ export class SmartCertManager {
// Check if provisioning is already in progress (prevent concurrent provisioning) // Check if provisioning is already in progress (prevent concurrent provisioning)
if (!allowConcurrent && this.isProvisioning) { if (!allowConcurrent && this.isProvisioning) {
console.log(`Certificate provisioning already in progress, skipping ${route.name}`); logger.log('info', `Certificate provisioning already in progress, skipping ${route.name}`, { routeName: route.name, component: 'certificate-manager' });
return; return;
} }
const domains = this.extractDomainsFromRoute(route); const domains = this.extractDomainsFromRoute(route);
if (domains.length === 0) { if (domains.length === 0) {
console.warn(`Route ${route.name} has TLS termination but no domains`); logger.log('warn', `Route ${route.name} has TLS termination but no domains`, { routeName: route.name, component: 'certificate-manager' });
return; return;
} }
@ -219,7 +220,7 @@ export class SmartCertManager {
// Check if we already have a valid certificate // Check if we already have a valid certificate
const existingCert = await this.certStore.getCertificate(routeName); const existingCert = await this.certStore.getCertificate(routeName);
if (existingCert && this.isCertificateValid(existingCert)) { if (existingCert && this.isCertificateValid(existingCert)) {
console.log(`Using existing valid certificate for ${primaryDomain}`); logger.log('info', `Using existing valid certificate for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
await this.applyCertificate(primaryDomain, existingCert); await this.applyCertificate(primaryDomain, existingCert);
this.updateCertStatus(routeName, 'valid', 'acme', existingCert); this.updateCertStatus(routeName, 'valid', 'acme', existingCert);
return; return;
@ -230,7 +231,7 @@ export class SmartCertManager {
this.globalAcmeDefaults?.renewThresholdDays || this.globalAcmeDefaults?.renewThresholdDays ||
30; 30;
console.log(`Requesting ACME certificate for ${domains.join(', ')} (renew ${renewThreshold} days before expiry)`); logger.log('info', `Requesting ACME certificate for ${domains.join(', ')} (renew ${renewThreshold} days before expiry)`, { domains: domains.join(', '), renewThreshold, component: 'certificate-manager' });
this.updateCertStatus(routeName, 'pending', 'acme'); this.updateCertStatus(routeName, 'pending', 'acme');
try { try {
@ -252,7 +253,7 @@ export class SmartCertManager {
hasDnsChallenge; hasDnsChallenge;
if (shouldIncludeWildcard) { if (shouldIncludeWildcard) {
console.log(`Requesting wildcard certificate for ${primaryDomain} (DNS-01 available)`); logger.log('info', `Requesting wildcard certificate for ${primaryDomain} (DNS-01 available)`, { domain: primaryDomain, challengeType: 'DNS-01', component: 'certificate-manager' });
} }
// Use smartacme to get certificate with optional wildcard // Use smartacme to get certificate with optional wildcard
@ -279,9 +280,9 @@ export class SmartCertManager {
await this.applyCertificate(primaryDomain, certData); await this.applyCertificate(primaryDomain, certData);
this.updateCertStatus(routeName, 'valid', 'acme', certData); this.updateCertStatus(routeName, 'valid', 'acme', certData);
console.log(`Successfully provisioned ACME certificate for ${primaryDomain}`); logger.log('info', `Successfully provisioned ACME certificate for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
} catch (error) { } catch (error) {
console.error(`Failed to provision ACME certificate for ${primaryDomain}: ${error}`); logger.log('error', `Failed to provision ACME certificate for ${primaryDomain}: ${error.message}`, { domain: primaryDomain, error: error.message, component: 'certificate-manager' });
this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message); this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
throw error; throw error;
} }
@ -328,9 +329,9 @@ export class SmartCertManager {
await this.applyCertificate(domain, certData); await this.applyCertificate(domain, certData);
this.updateCertStatus(routeName, 'valid', 'static', certData); this.updateCertStatus(routeName, 'valid', 'static', certData);
console.log(`Successfully loaded static certificate for ${domain}`); logger.log('info', `Successfully loaded static certificate for ${domain}`, { domain, component: 'certificate-manager' });
} catch (error) { } catch (error) {
console.error(`Failed to provision static certificate for ${domain}: ${error}`); logger.log('error', `Failed to provision static certificate for ${domain}: ${error.message}`, { domain, error: error.message, component: 'certificate-manager' });
this.updateCertStatus(routeName, 'error', 'static', undefined, error.message); this.updateCertStatus(routeName, 'error', 'static', undefined, error.message);
throw error; throw error;
} }
@ -341,7 +342,7 @@ export class SmartCertManager {
*/ */
private async applyCertificate(domain: string, certData: ICertificateData): Promise<void> { private async applyCertificate(domain: string, certData: ICertificateData): Promise<void> {
if (!this.httpProxy) { if (!this.httpProxy) {
console.warn('HttpProxy not set, cannot apply certificate'); logger.log('warn', `HttpProxy not set, cannot apply certificate for domain ${domain}`, { domain, component: 'certificate-manager' });
return; return;
} }
@ -398,13 +399,13 @@ export class SmartCertManager {
private async addChallengeRoute(): Promise<void> { private async addChallengeRoute(): Promise<void> {
// Check with state manager first // Check with state manager first
if (this.acmeStateManager && this.acmeStateManager.isChallengeRouteActive()) { if (this.acmeStateManager && this.acmeStateManager.isChallengeRouteActive()) {
console.log('Challenge route already active in global state, skipping'); logger.log('info', 'Challenge route already active in global state, skipping', { component: 'certificate-manager' });
this.challengeRouteActive = true; this.challengeRouteActive = true;
return; return;
} }
if (this.challengeRouteActive) { if (this.challengeRouteActive) {
console.log('Challenge route already active locally, skipping'); logger.log('info', 'Challenge route already active locally, skipping', { component: 'certificate-manager' });
return; return;
} }
@ -427,9 +428,9 @@ export class SmartCertManager {
this.acmeStateManager.addChallengeRoute(challengeRoute); this.acmeStateManager.addChallengeRoute(challengeRoute);
} }
console.log('ACME challenge route successfully added'); logger.log('info', 'ACME challenge route successfully added', { component: 'certificate-manager' });
} catch (error) { } catch (error) {
console.error('Failed to add challenge route:', error); logger.log('error', `Failed to add challenge route: ${error.message}`, { error: error.message, component: 'certificate-manager' });
if ((error as any).code === 'EADDRINUSE') { if ((error as any).code === 'EADDRINUSE') {
throw new Error(`Port ${this.globalAcmeDefaults?.port || 80} is already in use for ACME challenges`); throw new Error(`Port ${this.globalAcmeDefaults?.port || 80} is already in use for ACME challenges`);
} }
@ -442,7 +443,7 @@ export class SmartCertManager {
*/ */
private async removeChallengeRoute(): Promise<void> { private async removeChallengeRoute(): Promise<void> {
if (!this.challengeRouteActive) { if (!this.challengeRouteActive) {
console.log('Challenge route not active, skipping removal'); logger.log('info', 'Challenge route not active, skipping removal', { component: 'certificate-manager' });
return; return;
} }
@ -460,9 +461,9 @@ export class SmartCertManager {
this.acmeStateManager.removeChallengeRoute('acme-challenge'); this.acmeStateManager.removeChallengeRoute('acme-challenge');
} }
console.log('ACME challenge route successfully removed'); logger.log('info', 'ACME challenge route successfully removed', { component: 'certificate-manager' });
} catch (error) { } catch (error) {
console.error('Failed to remove challenge route:', error); logger.log('error', `Failed to remove challenge route: ${error.message}`, { error: error.message, component: 'certificate-manager' });
// Reset the flag even on error to avoid getting stuck // Reset the flag even on error to avoid getting stuck
this.challengeRouteActive = false; this.challengeRouteActive = false;
throw error; throw error;
@ -492,11 +493,11 @@ export class SmartCertManager {
const cert = await this.certStore.getCertificate(routeName); const cert = await this.certStore.getCertificate(routeName);
if (cert && !this.isCertificateValid(cert)) { if (cert && !this.isCertificateValid(cert)) {
console.log(`Certificate for ${routeName} needs renewal`); logger.log('info', `Certificate for ${routeName} needs renewal`, { routeName, component: 'certificate-manager' });
try { try {
await this.provisionCertificate(route); await this.provisionCertificate(route);
} catch (error) { } catch (error) {
console.error(`Failed to renew certificate for ${routeName}: ${error}`); logger.log('error', `Failed to renew certificate for ${routeName}: ${error.message}`, { routeName, error: error.message, component: 'certificate-manager' });
} }
} }
} }
@ -621,7 +622,7 @@ export class SmartCertManager {
// Always remove challenge route on shutdown // Always remove challenge route on shutdown
if (this.challengeRoute) { if (this.challengeRoute) {
console.log('Removing ACME challenge route during shutdown'); logger.log('info', 'Removing ACME challenge route during shutdown', { component: 'certificate-manager' });
await this.removeChallengeRoute(); await this.removeChallengeRoute();
} }

View File

@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js'; import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
import { SecurityManager } from './security-manager.js'; import { SecurityManager } from './security-manager.js';
import { TimeoutManager } from './timeout-manager.js'; import { TimeoutManager } from './timeout-manager.js';
import { logger } from '../../core/utils/logger.js';
/** /**
* Manages connection lifecycle, tracking, and cleanup * Manages connection lifecycle, tracking, and cleanup
@ -97,7 +98,7 @@ export class ConnectionManager {
*/ */
public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void { public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`); logger.log('info', `Connection cleanup initiated`, { connectionId: record.id, remoteIP: record.remoteIP, reason, component: 'connection-manager' });
} }
if ( if (
@ -139,7 +140,7 @@ export class ConnectionManager {
// Reset the handler references // Reset the handler references
record.renegotiationHandler = undefined; record.renegotiationHandler = undefined;
} catch (err) { } catch (err) {
console.log(`[${record.id}] Error removing data handlers: ${err}`); logger.log('error', `Error removing data handlers for connection ${record.id}: ${err}`, { connectionId: record.id, error: err, component: 'connection-manager' });
} }
} }
@ -160,16 +161,36 @@ export class ConnectionManager {
// Log connection details // Log connection details
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info',
`[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` + `Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}). ` +
` Duration: ${plugins.prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` + `Duration: ${plugins.prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` + `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` +
`${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` + `${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` +
`${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}` `${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`,
{
connectionId: record.id,
remoteIP: record.remoteIP,
localPort: record.localPort,
reason,
duration: plugins.prettyMs(duration),
bytes: { in: bytesReceived, out: bytesSent },
tls: record.isTLS,
keepAlive: record.hasKeepAlive,
usingNetworkProxy: record.usingNetworkProxy,
domainSwitches: record.domainSwitches || 0,
component: 'connection-manager'
}
); );
} else { } else {
console.log( logger.log('info',
`[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}` `Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`,
{
connectionId: record.id,
remoteIP: record.remoteIP,
reason,
activeConnections: this.connectionRecords.size,
component: 'connection-manager'
}
); );
} }
} }
@ -189,7 +210,7 @@ export class ConnectionManager {
socket.destroy(); socket.destroy();
} }
} catch (err) { } catch (err) {
console.log(`[${record.id}] Error destroying ${side} socket: ${err}`); logger.log('error', `Error destroying ${side} socket for connection ${record.id}: ${err}`, { connectionId: record.id, side, error: err, component: 'connection-manager' });
} }
}, 1000); }, 1000);
@ -199,13 +220,13 @@ export class ConnectionManager {
} }
} }
} catch (err) { } catch (err) {
console.log(`[${record.id}] Error closing ${side} socket: ${err}`); logger.log('error', `Error closing ${side} socket for connection ${record.id}: ${err}`, { connectionId: record.id, side, error: err, component: 'connection-manager' });
try { try {
if (!socket.destroyed) { if (!socket.destroyed) {
socket.destroy(); socket.destroy();
} }
} catch (destroyErr) { } catch (destroyErr) {
console.log(`[${record.id}] Error destroying ${side} socket: ${destroyErr}`); logger.log('error', `Error destroying ${side} socket for connection ${record.id}: ${destroyErr}`, { connectionId: record.id, side, error: destroyErr, component: 'connection-manager' });
} }
} }
} }
@ -224,21 +245,36 @@ export class ConnectionManager {
if (code === 'ECONNRESET') { if (code === 'ECONNRESET') {
reason = 'econnreset'; reason = 'econnreset';
console.log( logger.log('warn', `ECONNRESET on ${side} connection from ${record.remoteIP}. Error: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)}`, {
`[${record.id}] ECONNRESET on ${side} side from ${record.remoteIP}: ${err.message}. ` + connectionId: record.id,
`Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago` side,
); remoteIP: record.remoteIP,
error: err.message,
duration: plugins.prettyMs(connectionDuration),
lastActivity: plugins.prettyMs(lastActivityAge),
component: 'connection-manager'
});
} else if (code === 'ETIMEDOUT') { } else if (code === 'ETIMEDOUT') {
reason = 'etimedout'; reason = 'etimedout';
console.log( logger.log('warn', `ETIMEDOUT on ${side} connection from ${record.remoteIP}. Error: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)}`, {
`[${record.id}] ETIMEDOUT on ${side} side from ${record.remoteIP}: ${err.message}. ` + connectionId: record.id,
`Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago` side,
); remoteIP: record.remoteIP,
error: err.message,
duration: plugins.prettyMs(connectionDuration),
lastActivity: plugins.prettyMs(lastActivityAge),
component: 'connection-manager'
});
} else { } else {
console.log( logger.log('error', `Error on ${side} connection from ${record.remoteIP}: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)}`, {
`[${record.id}] Error on ${side} side from ${record.remoteIP}: ${err.message}. ` + connectionId: record.id,
`Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago` side,
); remoteIP: record.remoteIP,
error: err.message,
duration: plugins.prettyMs(connectionDuration),
lastActivity: plugins.prettyMs(lastActivityAge),
component: 'connection-manager'
});
} }
if (side === 'incoming' && record.incomingTerminationReason === null) { if (side === 'incoming' && record.incomingTerminationReason === null) {
@ -259,7 +295,12 @@ export class ConnectionManager {
public handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) { public handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
return () => { return () => {
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log(`[${record.id}] Connection closed on ${side} side from ${record.remoteIP}`); logger.log('info', `Connection closed on ${side} side`, {
connectionId: record.id,
side,
remoteIP: record.remoteIP,
component: 'connection-manager'
});
} }
if (side === 'incoming' && record.incomingTerminationReason === null) { if (side === 'incoming' && record.incomingTerminationReason === null) {
@ -321,11 +362,13 @@ export class ConnectionManager {
if (inactivityTime > effectiveTimeout && !record.connectionClosed) { if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
// For keep-alive connections, issue a warning first // For keep-alive connections, issue a warning first
if (record.hasKeepAlive && !record.inactivityWarningIssued) { if (record.hasKeepAlive && !record.inactivityWarningIssued) {
console.log( logger.log('warn', `Keep-alive connection ${id} from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. Will close in 10 minutes if no activity.`, {
`[${id}] Warning: Keep-alive connection from ${record.remoteIP} inactive for ${ connectionId: id,
plugins.prettyMs(inactivityTime) remoteIP: record.remoteIP,
}. Will close in 10 minutes if no activity.` inactiveFor: plugins.prettyMs(inactivityTime),
); closureWarning: '10 minutes',
component: 'connection-manager'
});
// Set warning flag and add grace period // Set warning flag and add grace period
record.inactivityWarningIssued = true; record.inactivityWarningIssued = true;
@ -337,27 +380,30 @@ export class ConnectionManager {
record.outgoing.write(Buffer.alloc(0)); record.outgoing.write(Buffer.alloc(0));
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log(`[${id}] Sent probe packet to test keep-alive connection`); logger.log('info', `Sent probe packet to test keep-alive connection ${id}`, { connectionId: id, component: 'connection-manager' });
} }
} catch (err) { } catch (err) {
console.log(`[${id}] Error sending probe packet: ${err}`); logger.log('error', `Error sending probe packet to connection ${id}: ${err}`, { connectionId: id, error: err, component: 'connection-manager' });
} }
} }
} else { } else {
// For non-keep-alive or after warning, close the connection // For non-keep-alive or after warning, close the connection
console.log( logger.log('warn', `Closing inactive connection ${id} from ${record.remoteIP} (inactive for ${plugins.prettyMs(inactivityTime)}, keep-alive: ${record.hasKeepAlive ? 'Yes' : 'No'})`, {
`[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` + connectionId: id,
`for ${plugins.prettyMs(inactivityTime)}.` + remoteIP: record.remoteIP,
(record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '') inactiveFor: plugins.prettyMs(inactivityTime),
); hasKeepAlive: record.hasKeepAlive,
component: 'connection-manager'
});
this.cleanupConnection(record, 'inactivity'); this.cleanupConnection(record, 'inactivity');
} }
} else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) { } else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
// If activity detected after warning, clear the warning // If activity detected after warning, clear the warning
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info', `Connection ${id} activity detected after inactivity warning`, {
`[${id}] Connection activity detected after inactivity warning, resetting warning` connectionId: id,
); component: 'connection-manager'
});
} }
record.inactivityWarningIssued = false; record.inactivityWarningIssued = false;
} }
@ -369,11 +415,12 @@ export class ConnectionManager {
!record.connectionClosed && !record.connectionClosed &&
now - record.outgoingClosedTime > 120000 now - record.outgoingClosedTime > 120000
) { ) {
console.log( logger.log('warn', `Parity check: Connection ${id} from ${record.remoteIP} has incoming socket still active ${plugins.prettyMs(now - record.outgoingClosedTime)} after outgoing socket closed`, {
`[${id}] Parity check: Incoming socket for ${record.remoteIP} still active ${ connectionId: id,
plugins.prettyMs(now - record.outgoingClosedTime) remoteIP: record.remoteIP,
} after outgoing closed.` timeElapsed: plugins.prettyMs(now - record.outgoingClosedTime),
); component: 'connection-manager'
});
this.cleanupConnection(record, 'parity_check'); this.cleanupConnection(record, 'parity_check');
} }
} }
@ -406,7 +453,7 @@ export class ConnectionManager {
record.outgoing.end(); record.outgoing.end();
} }
} catch (err) { } catch (err) {
console.log(`Error during graceful connection end for ${id}: ${err}`); logger.log('error', `Error during graceful end of connection ${id}: ${err}`, { connectionId: id, error: err, component: 'connection-manager' });
} }
} }
} }
@ -433,7 +480,7 @@ export class ConnectionManager {
} }
} }
} catch (err) { } catch (err) {
console.log(`Error during forced connection destruction for ${id}: ${err}`); logger.log('error', `Error during forced destruction of connection ${id}: ${err}`, { connectionId: id, error: err, component: 'connection-manager' });
} }
} }
} }

View File

@ -1,5 +1,6 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js'; import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
import { logger } from '../../core/utils/logger.js';
// Route checking functions have been removed // Route checking functions have been removed
import type { IRouteConfig, IRouteAction, IRouteContext } from './models/route-types.js'; import type { IRouteConfig, IRouteAction, IRouteContext } from './models/route-types.js';
import { ConnectionManager } from './connection-manager.js'; import { ConnectionManager } from './connection-manager.js';
@ -83,7 +84,7 @@ export class RouteConnectionHandler {
// Validate IP against rate limits and connection limits // Validate IP against rate limits and connection limits
const ipValidation = this.securityManager.validateIP(remoteIP); const ipValidation = this.securityManager.validateIP(remoteIP);
if (!ipValidation.allowed) { if (!ipValidation.allowed) {
console.log(`Connection rejected from ${remoteIP}: ${ipValidation.reason}`); logger.log('warn', `Connection rejected`, { remoteIP, reason: ipValidation.reason, component: 'route-handler' });
socket.end(); socket.end();
socket.destroy(); socket.destroy();
return; return;
@ -114,21 +115,35 @@ export class RouteConnectionHandler {
} catch (err) { } catch (err) {
// Ignore errors - these are optional enhancements // Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`); logger.log('warn', `Enhanced TCP keep-alive settings not supported`, { connectionId, error: err, component: 'route-handler' });
} }
} }
} }
} }
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info',
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` + `New connection from ${remoteIP} on port ${localPort}. ` +
`Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` + `Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
`Active connections: ${this.connectionManager.getConnectionCount()}` `Active connections: ${this.connectionManager.getConnectionCount()}`,
{
connectionId,
remoteIP,
localPort,
keepAlive: record.hasKeepAlive ? 'Enabled' : 'Disabled',
activeConnections: this.connectionManager.getConnectionCount(),
component: 'route-handler'
}
); );
} else { } else {
console.log( logger.log('info',
`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionManager.getConnectionCount()}` `New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionManager.getConnectionCount()}`,
{
remoteIP,
localPort,
activeConnections: this.connectionManager.getConnectionCount(),
component: 'route-handler'
}
); );
} }
@ -147,14 +162,20 @@ export class RouteConnectionHandler {
// Set an initial timeout for handshake data // Set an initial timeout for handshake data
let initialTimeout: NodeJS.Timeout | null = setTimeout(() => { let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
if (!initialDataReceived) { if (!initialDataReceived) {
console.log( logger.log('warn', `No initial data received from ${record.remoteIP} after ${this.settings.initialDataTimeout}ms for connection ${connectionId}`, {
`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}` connectionId,
); timeout: this.settings.initialDataTimeout,
remoteIP: record.remoteIP,
component: 'route-handler'
});
// Add a grace period // Add a grace period
setTimeout(() => { setTimeout(() => {
if (!initialDataReceived) { if (!initialDataReceived) {
console.log(`[${connectionId}] Final initial data timeout after grace period`); logger.log('warn', `Final initial data timeout after grace period for connection ${connectionId}`, {
connectionId,
component: 'route-handler'
});
if (record.incomingTerminationReason === null) { if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'initial_timeout'; record.incomingTerminationReason = 'initial_timeout';
this.connectionManager.incrementTerminationStat('incoming', 'initial_timeout'); this.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
@ -187,10 +208,11 @@ export class RouteConnectionHandler {
// Block non-TLS connections on port 443 // Block non-TLS connections on port 443
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) { if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
console.log( logger.log('warn', `Non-TLS connection ${connectionId} detected on port 443. Terminating connection - only TLS traffic is allowed on standard HTTPS port.`, {
`[${connectionId}] Non-TLS connection detected on port 443. ` + connectionId,
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.` message: 'Terminating connection - only TLS traffic is allowed on standard HTTPS port.',
); component: 'route-handler'
});
if (record.incomingTerminationReason === null) { if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'non_tls_blocked'; record.incomingTerminationReason = 'non_tls_blocked';
this.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked'); this.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
@ -223,7 +245,10 @@ export class RouteConnectionHandler {
// Check if we should reject connections without SNI // Check if we should reject connections without SNI
if (!serverName && this.settings.allowSessionTicket === false) { if (!serverName && this.settings.allowSessionTicket === false) {
console.log(`[${connectionId}] No SNI detected in TLS ClientHello; sending TLS alert.`); logger.log('warn', `No SNI detected in TLS ClientHello for connection ${connectionId}; sending TLS alert`, {
connectionId,
component: 'route-handler'
});
if (record.incomingTerminationReason === null) { if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'session_ticket_blocked_no_sni'; record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
this.connectionManager.incrementTerminationStat( this.connectionManager.incrementTerminationStat(
@ -245,7 +270,11 @@ export class RouteConnectionHandler {
} }
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] TLS connection with SNI: ${serverName || '(empty)'}`); logger.log('info', `TLS connection with SNI`, {
connectionId,
serverName: serverName || '(empty)',
component: 'route-handler'
});
} }
} }
} }
@ -278,12 +307,18 @@ export class RouteConnectionHandler {
}); });
if (!routeMatch) { if (!routeMatch) {
console.log( logger.log('warn', `No route found for ${serverName || 'connection'} on port ${localPort} (connection: ${connectionId})`, {
`[${connectionId}] No route found for ${serverName || 'connection'} on port ${localPort}` connectionId,
); serverName: serverName || 'connection',
localPort,
component: 'route-handler'
});
// No matching route, use default/fallback handling // No matching route, use default/fallback handling
console.log(`[${connectionId}] Using default route handling for connection`); logger.log('info', `Using default route handling for connection ${connectionId}`, {
connectionId,
component: 'route-handler'
});
// Check default security settings // Check default security settings
const defaultSecuritySettings = this.settings.defaults?.security; const defaultSecuritySettings = this.settings.defaults?.security;
@ -296,7 +331,11 @@ export class RouteConnectionHandler {
); );
if (!isAllowed) { if (!isAllowed) {
console.log(`[${connectionId}] IP ${remoteIP} not in default allowed list`); logger.log('warn', `IP ${remoteIP} not in default allowed list for connection ${connectionId}`, {
connectionId,
remoteIP,
component: 'route-handler'
});
socket.end(); socket.end();
this.connectionManager.cleanupConnection(record, 'ip_blocked'); this.connectionManager.cleanupConnection(record, 'ip_blocked');
return; return;
@ -321,7 +360,10 @@ export class RouteConnectionHandler {
); );
} else { } else {
// No default target available, terminate the connection // No default target available, terminate the connection
console.log(`[${connectionId}] No default target configured. Closing connection.`); logger.log('warn', `No default target configured for connection ${connectionId}. Closing connection`, {
connectionId,
component: 'route-handler'
});
socket.end(); socket.end();
this.connectionManager.cleanupConnection(record, 'no_default_target'); this.connectionManager.cleanupConnection(record, 'no_default_target');
return; return;
@ -332,11 +374,13 @@ export class RouteConnectionHandler {
const route = routeMatch.route; const route = routeMatch.route;
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info', `Route matched`, {
`[${connectionId}] Route matched: "${route.name || 'unnamed'}" for ${ connectionId,
serverName || 'connection' routeName: route.name || 'unnamed',
} on port ${localPort}` serverName: serverName || 'connection',
); localPort,
component: 'route-handler'
});
} }
@ -356,7 +400,11 @@ export class RouteConnectionHandler {
return; return;
default: default:
console.log(`[${connectionId}] Unknown action type: ${(route.action as any).type}`); logger.log('error', `Unknown action type '${(route.action as any).type}' for connection ${connectionId}`, {
connectionId,
actionType: (route.action as any).type,
component: 'route-handler'
});
socket.end(); socket.end();
this.connectionManager.cleanupConnection(record, 'unknown_action'); this.connectionManager.cleanupConnection(record, 'unknown_action');
} }
@ -381,30 +429,36 @@ export class RouteConnectionHandler {
// Log the connection for monitoring purposes // Log the connection for monitoring purposes
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info', `NFTables forwarding (kernel-level)`, {
`[${record.id}] NFTables forwarding (kernel-level): ` + connectionId: record.id,
`${record.remoteIP}:${socket.remotePort} -> ${socket.localAddress}:${record.localPort}` + source: `${record.remoteIP}:${socket.remotePort}`,
` (Route: "${route.name || 'unnamed'}", Domain: ${record.lockedDomain || 'n/a'})` destination: `${socket.localAddress}:${record.localPort}`,
); routeName: route.name || 'unnamed',
domain: record.lockedDomain || 'n/a',
component: 'route-handler'
});
} else { } else {
console.log( logger.log('info', `NFTables forwarding`, {
`[${record.id}] NFTables forwarding: ${record.remoteIP} -> port ${ connectionId: record.id,
record.localPort remoteIP: record.remoteIP,
} (Route: "${route.name || 'unnamed'}")` localPort: record.localPort,
); routeName: route.name || 'unnamed',
component: 'route-handler'
});
} }
// Additional NFTables-specific logging if configured // Additional NFTables-specific logging if configured
if (action.nftables) { if (action.nftables) {
const nftConfig = action.nftables; const nftConfig = action.nftables;
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info', `NFTables config`, {
`[${record.id}] NFTables config: ` + connectionId: record.id,
`protocol=${nftConfig.protocol || 'tcp'}, ` + protocol: nftConfig.protocol || 'tcp',
`preserveSourceIP=${nftConfig.preserveSourceIP || false}, ` + preserveSourceIP: nftConfig.preserveSourceIP || false,
`priority=${nftConfig.priority || 'default'}, ` + priority: nftConfig.priority || 'default',
`maxRate=${nftConfig.maxRate || 'unlimited'}` maxRate: nftConfig.maxRate || 'unlimited',
); component: 'route-handler'
});
} }
} }
@ -419,7 +473,10 @@ export class RouteConnectionHandler {
// We should have a target configuration for forwarding // We should have a target configuration for forwarding
if (!action.target) { if (!action.target) {
console.log(`[${connectionId}] Forward action missing target configuration`); logger.log('error', `Forward action missing target configuration for connection ${connectionId}`, {
connectionId,
component: 'route-handler'
});
socket.end(); socket.end();
this.connectionManager.cleanupConnection(record, 'missing_target'); this.connectionManager.cleanupConnection(record, 'missing_target');
return; return;
@ -447,14 +504,18 @@ export class RouteConnectionHandler {
try { try {
targetHost = action.target.host(routeContext); targetHost = action.target.host(routeContext);
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info', `Dynamic host resolved to ${Array.isArray(targetHost) ? targetHost.join(', ') : targetHost} for connection ${connectionId}`, {
`[${connectionId}] Dynamic host resolved to: ${ connectionId,
Array.isArray(targetHost) ? targetHost.join(', ') : targetHost targetHost: Array.isArray(targetHost) ? targetHost.join(', ') : targetHost,
}` component: 'route-handler'
); });
} }
} catch (err) { } catch (err) {
console.log(`[${connectionId}] Error in host mapping function: ${err}`); logger.log('error', `Error in host mapping function for connection ${connectionId}: ${err}`, {
connectionId,
error: err,
component: 'route-handler'
});
socket.end(); socket.end();
this.connectionManager.cleanupConnection(record, 'host_mapping_error'); this.connectionManager.cleanupConnection(record, 'host_mapping_error');
return; return;
@ -474,14 +535,21 @@ export class RouteConnectionHandler {
try { try {
targetPort = action.target.port(routeContext); targetPort = action.target.port(routeContext);
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info', `Dynamic port mapping from ${record.localPort} to ${targetPort} for connection ${connectionId}`, {
`[${connectionId}] Dynamic port mapping: ${record.localPort} -> ${targetPort}` connectionId,
); sourcePort: record.localPort,
targetPort,
component: 'route-handler'
});
} }
// Store the resolved target port in the context for potential future use // Store the resolved target port in the context for potential future use
routeContext.targetPort = targetPort; routeContext.targetPort = targetPort;
} catch (err) { } catch (err) {
console.log(`[${connectionId}] Error in port mapping function: ${err}`); logger.log('error', `Error in port mapping function for connection ${connectionId}: ${err}`, {
connectionId,
error: err,
component: 'route-handler'
});
socket.end(); socket.end();
this.connectionManager.cleanupConnection(record, 'port_mapping_error'); this.connectionManager.cleanupConnection(record, 'port_mapping_error');
return; return;
@ -503,7 +571,12 @@ export class RouteConnectionHandler {
case 'passthrough': case 'passthrough':
// For TLS passthrough, just forward directly // For TLS passthrough, just forward directly
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Using TLS passthrough to ${selectedHost}:${targetPort}`); logger.log('info', `Using TLS passthrough to ${selectedHost}:${targetPort} for connection ${connectionId}`, {
connectionId,
targetHost: selectedHost,
targetPort,
component: 'route-handler'
});
} }
return this.setupDirectConnection( return this.setupDirectConnection(
@ -521,9 +594,11 @@ export class RouteConnectionHandler {
// For TLS termination, use HttpProxy // For TLS termination, use HttpProxy
if (this.httpProxyBridge.getHttpProxy()) { if (this.httpProxyBridge.getHttpProxy()) {
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info', `Using HttpProxy for TLS termination to ${Array.isArray(action.target.host) ? action.target.host.join(', ') : action.target.host} for connection ${connectionId}`, {
`[${connectionId}] Using HttpProxy for TLS termination to ${action.target.host}` connectionId,
); targetHost: action.target.host,
component: 'route-handler'
});
} }
// If we have an initial chunk with TLS data, start processing it // If we have an initial chunk with TLS data, start processing it
@ -540,12 +615,18 @@ export class RouteConnectionHandler {
} }
// This shouldn't normally happen - we should have TLS data at this point // This shouldn't normally happen - we should have TLS data at this point
console.log(`[${connectionId}] TLS termination route without TLS data`); logger.log('error', `TLS termination route without TLS data for connection ${connectionId}`, {
connectionId,
component: 'route-handler'
});
socket.end(); socket.end();
this.connectionManager.cleanupConnection(record, 'tls_error'); this.connectionManager.cleanupConnection(record, 'tls_error');
return; return;
} else { } else {
console.log(`[${connectionId}] HttpProxy not available for TLS termination`); logger.log('error', `HttpProxy not available for TLS termination for connection ${connectionId}`, {
connectionId,
component: 'route-handler'
});
socket.end(); socket.end();
this.connectionManager.cleanupConnection(record, 'no_http_proxy'); this.connectionManager.cleanupConnection(record, 'no_http_proxy');
return; return;
@ -558,9 +639,11 @@ export class RouteConnectionHandler {
if (isHttpProxyPort && this.httpProxyBridge.getHttpProxy()) { if (isHttpProxyPort && this.httpProxyBridge.getHttpProxy()) {
// Forward non-TLS connections to HttpProxy if configured // Forward non-TLS connections to HttpProxy if configured
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info', `Using HttpProxy for non-TLS connection ${connectionId} on port ${record.localPort}`, {
`[${connectionId}] Using HttpProxy for non-TLS connection on port ${record.localPort}` connectionId,
); port: record.localPort,
component: 'route-handler'
});
} }
this.httpProxyBridge.forwardToHttpProxy( this.httpProxyBridge.forwardToHttpProxy(
@ -575,9 +658,12 @@ export class RouteConnectionHandler {
} else { } else {
// Basic forwarding // Basic forwarding
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info', `Using basic forwarding to ${Array.isArray(action.target.host) ? action.target.host.join(', ') : action.target.host}:${action.target.port} for connection ${connectionId}`, {
`[${connectionId}] Using basic forwarding to ${action.target.host}:${action.target.port}` connectionId,
); targetHost: action.target.host,
targetPort: action.target.port,
component: 'route-handler'
});
} }
// Get the appropriate host value // Get the appropriate host value
@ -633,7 +719,10 @@ export class RouteConnectionHandler {
): void { ): void {
// For TLS connections, we can't do redirects at the TCP level // For TLS connections, we can't do redirects at the TCP level
if (record.isTLS) { if (record.isTLS) {
console.log(`[${record.id}] Cannot redirect TLS connection at TCP level`); logger.log('warn', `Cannot redirect TLS connection ${record.id} at TCP level`, {
connectionId: record.id,
component: 'route-handler'
});
socket.end(); socket.end();
this.connectionManager.cleanupConnection(record, 'tls_redirect_error'); this.connectionManager.cleanupConnection(record, 'tls_redirect_error');
return; return;
@ -658,9 +747,11 @@ export class RouteConnectionHandler {
const connectionId = record.id; const connectionId = record.id;
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info', `Blocking connection ${connectionId} based on route '${route.name || 'unnamed'}'`, {
`[${connectionId}] Blocking connection based on route "${route.name || 'unnamed'}"` connectionId,
); routeName: route.name || 'unnamed',
component: 'route-handler'
});
} }
// Simply close the connection // Simply close the connection
@ -699,8 +790,16 @@ export class RouteConnectionHandler {
targetSocket.once('error', (err) => { targetSocket.once('error', (err) => {
// This handler runs only once during the initial connection phase // This handler runs only once during the initial connection phase
const code = (err as any).code; const code = (err as any).code;
console.log( logger.log('error',
`[${connectionId}] Connection setup error to ${finalTargetHost}:${finalTargetPort}: ${err.message} (${code})` `Connection setup error for ${connectionId} to ${finalTargetHost}:${finalTargetPort}: ${err.message} (${code})`,
{
connectionId,
targetHost: finalTargetHost,
targetPort: finalTargetPort,
errorMessage: err.message,
errorCode: code,
component: 'route-handler'
}
); );
// Resume the incoming socket to prevent it from hanging // Resume the incoming socket to prevent it from hanging
@ -708,29 +807,57 @@ export class RouteConnectionHandler {
// Log specific error types for easier debugging // Log specific error types for easier debugging
if (code === 'ECONNREFUSED') { if (code === 'ECONNREFUSED') {
console.log( logger.log('error',
`[${connectionId}] Target ${finalTargetHost}:${finalTargetPort} refused connection. ` + `Connection ${connectionId}: Target ${finalTargetHost}:${finalTargetPort} refused connection. Check if the target service is running and listening on that port.`,
`Check if the target service is running and listening on that port.` {
connectionId,
targetHost: finalTargetHost,
targetPort: finalTargetPort,
recommendation: 'Check if the target service is running and listening on that port.',
component: 'route-handler'
}
); );
} else if (code === 'ETIMEDOUT') { } else if (code === 'ETIMEDOUT') {
console.log( logger.log('error',
`[${connectionId}] Connection to ${finalTargetHost}:${finalTargetPort} timed out. ` + `Connection ${connectionId} to ${finalTargetHost}:${finalTargetPort} timed out. Check network conditions, firewall rules, or if the target is too far away.`,
`Check network conditions, firewall rules, or if the target is too far away.` {
connectionId,
targetHost: finalTargetHost,
targetPort: finalTargetPort,
recommendation: 'Check network conditions, firewall rules, or if the target is too far away.',
component: 'route-handler'
}
); );
} else if (code === 'ECONNRESET') { } else if (code === 'ECONNRESET') {
console.log( logger.log('error',
`[${connectionId}] Connection to ${finalTargetHost}:${finalTargetPort} was reset. ` + `Connection ${connectionId} to ${finalTargetHost}:${finalTargetPort} was reset. The target might have closed the connection abruptly.`,
`The target might have closed the connection abruptly.` {
connectionId,
targetHost: finalTargetHost,
targetPort: finalTargetPort,
recommendation: 'The target might have closed the connection abruptly.',
component: 'route-handler'
}
); );
} else if (code === 'EHOSTUNREACH') { } else if (code === 'EHOSTUNREACH') {
console.log( logger.log('error',
`[${connectionId}] Host ${finalTargetHost} is unreachable. ` + `Connection ${connectionId}: Host ${finalTargetHost} is unreachable. Check DNS settings, network routing, or firewall rules.`,
`Check DNS settings, network routing, or firewall rules.` {
connectionId,
targetHost: finalTargetHost,
recommendation: 'Check DNS settings, network routing, or firewall rules.',
component: 'route-handler'
}
); );
} else if (code === 'ENOTFOUND') { } else if (code === 'ENOTFOUND') {
console.log( logger.log('error',
`[${connectionId}] DNS lookup failed for ${finalTargetHost}. ` + `Connection ${connectionId}: DNS lookup failed for ${finalTargetHost}. Check your DNS settings or if the hostname is correct.`,
`Check your DNS settings or if the hostname is correct.` {
connectionId,
targetHost: finalTargetHost,
recommendation: 'Check your DNS settings or if the hostname is correct.',
component: 'route-handler'
}
); );
} }
@ -779,9 +906,12 @@ export class RouteConnectionHandler {
record.targetPort = finalTargetPort; record.targetPort = finalTargetPort;
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info', `Setting up direct connection ${connectionId} to ${finalTargetHost}:${finalTargetPort}`, {
`[${connectionId}] Setting up direct connection to ${finalTargetHost}:${finalTargetPort}` connectionId,
); targetHost: finalTargetHost,
targetPort: finalTargetPort,
component: 'route-handler'
});
} }
// Setup connection options // Setup connection options
@ -826,9 +956,11 @@ export class RouteConnectionHandler {
} catch (err) { } catch (err) {
// Ignore errors - these are optional enhancements // Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('warn', `Enhanced TCP keep-alive not supported for outgoing socket on connection ${connectionId}: ${err}`, {
`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}` connectionId,
); error: err,
component: 'route-handler'
});
} }
} }
} }
@ -848,22 +980,23 @@ export class RouteConnectionHandler {
socket.on('timeout', () => { socket.on('timeout', () => {
// For keep-alive connections, just log a warning instead of closing // For keep-alive connections, just log a warning instead of closing
if (record.hasKeepAlive) { if (record.hasKeepAlive) {
console.log( logger.log('warn', `Timeout event on incoming keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}. Connection preserved.`, {
`[${connectionId}] Timeout event on incoming keep-alive connection from ${ connectionId,
record.remoteIP remoteIP: record.remoteIP,
} after ${plugins.prettyMs( timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000),
this.settings.socketTimeout || 3600000 status: 'Connection preserved',
)}. Connection preserved.` component: 'route-handler'
); });
return; return;
} }
// For non-keep-alive connections, proceed with normal cleanup // For non-keep-alive connections, proceed with normal cleanup
console.log( logger.log('warn', `Timeout on incoming side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`, {
`[${connectionId}] Timeout on incoming side from ${ connectionId,
record.remoteIP remoteIP: record.remoteIP,
} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}` timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000),
); component: 'route-handler'
});
if (record.incomingTerminationReason === null) { if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'timeout'; record.incomingTerminationReason = 'timeout';
this.connectionManager.incrementTerminationStat('incoming', 'timeout'); this.connectionManager.incrementTerminationStat('incoming', 'timeout');
@ -874,22 +1007,23 @@ export class RouteConnectionHandler {
targetSocket.on('timeout', () => { targetSocket.on('timeout', () => {
// For keep-alive connections, just log a warning instead of closing // For keep-alive connections, just log a warning instead of closing
if (record.hasKeepAlive) { if (record.hasKeepAlive) {
console.log( logger.log('warn', `Timeout event on outgoing keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}. Connection preserved.`, {
`[${connectionId}] Timeout event on outgoing keep-alive connection from ${ connectionId,
record.remoteIP remoteIP: record.remoteIP,
} after ${plugins.prettyMs( timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000),
this.settings.socketTimeout || 3600000 status: 'Connection preserved',
)}. Connection preserved.` component: 'route-handler'
); });
return; return;
} }
// For non-keep-alive connections, proceed with normal cleanup // For non-keep-alive connections, proceed with normal cleanup
console.log( logger.log('warn', `Timeout on outgoing side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`, {
`[${connectionId}] Timeout on outgoing side from ${ connectionId,
record.remoteIP remoteIP: record.remoteIP,
} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}` timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000),
); component: 'route-handler'
});
if (record.outgoingTerminationReason === null) { if (record.outgoingTerminationReason === null) {
record.outgoingTerminationReason = 'timeout'; record.outgoingTerminationReason = 'timeout';
this.connectionManager.incrementTerminationStat('outgoing', 'timeout'); this.connectionManager.incrementTerminationStat('outgoing', 'timeout');
@ -909,9 +1043,12 @@ export class RouteConnectionHandler {
// Wait for the outgoing connection to be ready before setting up piping // Wait for the outgoing connection to be ready before setting up piping
targetSocket.once('connect', () => { targetSocket.once('connect', () => {
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info', `Connection ${connectionId} established to target ${finalTargetHost}:${finalTargetPort}`, {
`[${connectionId}] Connection established to target: ${finalTargetHost}:${finalTargetPort}` connectionId,
); targetHost: finalTargetHost,
targetPort: finalTargetPort,
component: 'route-handler'
});
} }
// Clear the initial connection error handler // Clear the initial connection error handler
@ -933,7 +1070,11 @@ export class RouteConnectionHandler {
// Write pending data immediately // Write pending data immediately
targetSocket.write(combinedData, (err) => { targetSocket.write(combinedData, (err) => {
if (err) { if (err) {
console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`); logger.log('error', `Error writing pending data to target for connection ${connectionId}: ${err.message}`, {
connectionId,
error: err.message,
component: 'route-handler'
});
return this.connectionManager.initiateCleanupOnce(record, 'write_error'); return this.connectionManager.initiateCleanupOnce(record, 'write_error');
} }
}); });
@ -954,15 +1095,17 @@ export class RouteConnectionHandler {
}); });
// Log successful connection // Log successful connection
console.log( logger.log('info',
`Connection established: ${record.remoteIP} -> ${finalTargetHost}:${finalTargetPort}` + `Connection established: ${record.remoteIP} -> ${finalTargetHost}:${finalTargetPort}` +
`${ `${serverName ? ` (SNI: ${serverName})` : record.lockedDomain ? ` (Domain: ${record.lockedDomain})` : ''}`,
serverName {
? ` (SNI: ${serverName})` remoteIP: record.remoteIP,
: record.lockedDomain targetHost: finalTargetHost,
? ` (Domain: ${record.lockedDomain})` targetPort: finalTargetPort,
: '' sni: serverName || undefined,
}` domain: !serverName && record.lockedDomain ? record.lockedDomain : undefined,
component: 'route-handler'
}
); );
// Add TLS renegotiation handler if needed // Add TLS renegotiation handler if needed
@ -990,17 +1133,21 @@ export class RouteConnectionHandler {
socket.on('data', renegotiationHandler); socket.on('data', renegotiationHandler);
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( logger.log('info', `TLS renegotiation handler installed for connection ${connectionId} with SNI ${serverName}`, {
`[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}` connectionId,
); serverName,
component: 'route-handler'
});
} }
} }
// Set connection timeout // Set connection timeout
record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => { record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
console.log( logger.log('warn', `Connection ${connectionId} from ${record.remoteIP} exceeded max lifetime, forcing cleanup`, {
`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.` connectionId,
); remoteIP: record.remoteIP,
component: 'route-handler'
});
this.connectionManager.initiateCleanupOnce(record, reason); this.connectionManager.initiateCleanupOnce(record, reason);
}); });

View File

@ -1,4 +1,5 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import { logger } from '../../core/utils/logger.js';
// Importing required components // Importing required components
import { ConnectionManager } from './connection-manager.js'; import { ConnectionManager } from './connection-manager.js';
@ -239,7 +240,7 @@ export class SmartProxy extends plugins.EventEmitter {
); );
if (autoRoutes.length === 0 && !this.hasStaticCertRoutes()) { if (autoRoutes.length === 0 && !this.hasStaticCertRoutes()) {
console.log('No routes require certificate management'); logger.log('info', 'No routes require certificate management', { component: 'certificate-manager' });
return; return;
} }
@ -256,7 +257,7 @@ export class SmartProxy extends plugins.EventEmitter {
useProduction: this.settings.acme.useProduction || false, useProduction: this.settings.acme.useProduction || false,
port: this.settings.acme.port || 80 port: this.settings.acme.port || 80
}; };
console.log(`Using top-level ACME configuration with email: ${acmeOptions.email}`); logger.log('info', `Using top-level ACME configuration with email: ${acmeOptions.email}`, { component: 'certificate-manager' });
} else if (autoRoutes.length > 0) { } else if (autoRoutes.length > 0) {
// Check for route-level ACME config // Check for route-level ACME config
const routeWithAcme = autoRoutes.find(r => r.action.tls?.acme?.email); const routeWithAcme = autoRoutes.find(r => r.action.tls?.acme?.email);
@ -267,7 +268,7 @@ export class SmartProxy extends plugins.EventEmitter {
useProduction: routeAcme.useProduction || false, useProduction: routeAcme.useProduction || false,
port: routeAcme.challengePort || 80 port: routeAcme.challengePort || 80
}; };
console.log(`Using route-level ACME configuration from route '${routeWithAcme.name}' with email: ${acmeOptions.email}`); logger.log('info', `Using route-level ACME configuration from route '${routeWithAcme.name}' with email: ${acmeOptions.email}`, { component: 'certificate-manager' });
} }
} }
@ -305,7 +306,7 @@ export class SmartProxy extends plugins.EventEmitter {
public async start() { public async start() {
// Don't start if already shutting down // Don't start if already shutting down
if (this.isShuttingDown) { if (this.isShuttingDown) {
console.log("Cannot start SmartProxy while it's shutting down"); logger.log('warn', "Cannot start SmartProxy while it's in the shutdown process");
return; return;
} }
@ -332,9 +333,9 @@ export class SmartProxy extends plugins.EventEmitter {
const allWarnings = [...configWarnings, ...acmeWarnings]; const allWarnings = [...configWarnings, ...acmeWarnings];
if (allWarnings.length > 0) { if (allWarnings.length > 0) {
console.log("Configuration warnings:"); logger.log('warn', `${allWarnings.length} configuration warnings found`, { count: allWarnings.length });
for (const warning of allWarnings) { for (const warning of allWarnings) {
console.log(` - ${warning}`); logger.log('warn', `${warning}`);
} }
} }
@ -353,7 +354,7 @@ export class SmartProxy extends plugins.EventEmitter {
// Now that ports are listening, provision any required certificates // Now that ports are listening, provision any required certificates
if (this.certManager) { if (this.certManager) {
console.log('Starting certificate provisioning now that ports are ready'); logger.log('info', 'Starting certificate provisioning now that ports are ready', { component: 'certificate-manager' });
await this.certManager.provisionAllCertificates(); await this.certManager.provisionAllCertificates();
} }
@ -411,16 +412,26 @@ export class SmartProxy extends plugins.EventEmitter {
const terminationStats = this.connectionManager.getTerminationStats(); const terminationStats = this.connectionManager.getTerminationStats();
// Log detailed stats // Log detailed stats
console.log( logger.log('info', 'Connection statistics', {
`Active connections: ${connectionRecords.size}. ` + activeConnections: connectionRecords.size,
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` + tls: {
`Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, HttpProxy=${httpProxyConnections}. ` + total: tlsConnections,
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` + completed: completedTlsHandshakes,
`Termination stats: ${JSON.stringify({ pending: pendingTlsHandshakes
IN: terminationStats.incoming, },
OUT: terminationStats.outgoing, nonTls: nonTlsConnections,
})}` keepAlive: keepAliveConnections,
); httpProxy: httpProxyConnections,
longestRunning: {
incoming: plugins.prettyMs(maxIncoming),
outgoing: plugins.prettyMs(maxOutgoing)
},
terminationStats: {
incoming: terminationStats.incoming,
outgoing: terminationStats.outgoing
},
component: 'connection-manager'
});
}, this.settings.inactivityCheckInterval || 60000); }, this.settings.inactivityCheckInterval || 60000);
// Make sure the interval doesn't keep the process alive // Make sure the interval doesn't keep the process alive
@ -439,19 +450,19 @@ export class SmartProxy extends plugins.EventEmitter {
* Stop the proxy server * Stop the proxy server
*/ */
public async stop() { public async stop() {
console.log('SmartProxy shutting down...'); logger.log('info', 'SmartProxy shutting down...');
this.isShuttingDown = true; this.isShuttingDown = true;
this.portManager.setShuttingDown(true); this.portManager.setShuttingDown(true);
// Stop certificate manager // Stop certificate manager
if (this.certManager) { if (this.certManager) {
await this.certManager.stop(); await this.certManager.stop();
console.log('Certificate manager stopped'); logger.log('info', 'Certificate manager stopped');
} }
// Stop NFTablesManager // Stop NFTablesManager
await this.nftablesManager.stop(); await this.nftablesManager.stop();
console.log('NFTablesManager stopped'); logger.log('info', 'NFTablesManager stopped');
// Stop the connection logger // Stop the connection logger
if (this.connectionLogger) { if (this.connectionLogger) {
@ -461,7 +472,7 @@ export class SmartProxy extends plugins.EventEmitter {
// Stop all port listeners // Stop all port listeners
await this.portManager.closeAll(); await this.portManager.closeAll();
console.log('All servers closed. Cleaning up active connections...'); logger.log('info', 'All servers closed. Cleaning up active connections...');
// Clean up all active connections // Clean up all active connections
this.connectionManager.clearConnections(); this.connectionManager.clearConnections();
@ -472,7 +483,7 @@ export class SmartProxy extends plugins.EventEmitter {
// Clear ACME state manager // Clear ACME state manager
this.acmeStateManager.clear(); this.acmeStateManager.clear();
console.log('SmartProxy shutdown complete.'); logger.log('info', 'SmartProxy shutdown complete.');
} }
/** /**
@ -481,7 +492,7 @@ export class SmartProxy extends plugins.EventEmitter {
* Note: This legacy method has been removed. Use updateRoutes instead. * Note: This legacy method has been removed. Use updateRoutes instead.
*/ */
public async updateDomainConfigs(): Promise<void> { public async updateDomainConfigs(): Promise<void> {
console.warn('Method updateDomainConfigs() is deprecated. Use updateRoutes() instead.'); logger.log('warn', 'Method updateDomainConfigs() is deprecated. Use updateRoutes() instead.');
throw new Error('updateDomainConfigs() is deprecated - use updateRoutes() instead'); throw new Error('updateDomainConfigs() is deprecated - use updateRoutes() instead');
} }
@ -497,7 +508,7 @@ export class SmartProxy extends plugins.EventEmitter {
const challengeRouteExists = this.settings.routes.some(r => r.name === 'acme-challenge'); const challengeRouteExists = this.settings.routes.some(r => r.name === 'acme-challenge');
if (!challengeRouteExists) { if (!challengeRouteExists) {
console.log('Challenge route successfully removed from routes'); logger.log('info', 'Challenge route successfully removed from routes');
return; return;
} }
@ -505,7 +516,9 @@ export class SmartProxy extends plugins.EventEmitter {
await plugins.smartdelay.delayFor(retryDelay); await plugins.smartdelay.delayFor(retryDelay);
} }
throw new Error('Failed to verify challenge route removal after ' + maxRetries + ' attempts'); const error = `Failed to verify challenge route removal after ${maxRetries} attempts`;
logger.log('error', error);
throw new Error(error);
} }
/** /**
@ -533,7 +546,7 @@ export class SmartProxy extends plugins.EventEmitter {
*/ */
public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> { public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> {
return this.routeUpdateLock.runExclusive(async () => { return this.routeUpdateLock.runExclusive(async () => {
console.log(`Updating routes (${newRoutes.length} routes)`); logger.log('info', `Updating routes (${newRoutes.length} routes)`, { routeCount: newRoutes.length, component: 'route-manager' });
// Get existing routes that use NFTables // Get existing routes that use NFTables
const oldNfTablesRoutes = this.settings.routes.filter( const oldNfTablesRoutes = this.settings.routes.filter(
@ -658,14 +671,14 @@ export class SmartProxy extends plugins.EventEmitter {
// Check for wildcard domains (they can't get ACME certs) // Check for wildcard domains (they can't get ACME certs)
if (domain.includes('*')) { if (domain.includes('*')) {
console.log(`Wildcard domains like "${domain}" are not supported for ACME certificates`); logger.log('warn', `Wildcard domains like "${domain}" are not supported for automatic ACME certificates`, { domain, component: 'certificate-manager' });
return false; return false;
} }
// Check if domain has at least one dot and no invalid characters // 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])?)*$/; 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)) { if (!validDomainRegex.test(domain)) {
console.log(`Domain "${domain}" has invalid format`); logger.log('warn', `Domain "${domain}" has invalid format for certificate issuance`, { domain, component: 'certificate-manager' });
return false; return false;
} }