|
|
|
@ -10,10 +10,6 @@ export interface IDomainConfig {
|
|
|
|
|
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
|
|
|
|
// Allow domain-specific timeout override
|
|
|
|
|
connectionTimeout?: number; // Connection timeout override (ms)
|
|
|
|
|
|
|
|
|
|
// New properties for NetworkProxy integration
|
|
|
|
|
useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy
|
|
|
|
|
networkProxyIndex?: number; // Optional index to specify which NetworkProxy to use (defaults to 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Port proxy settings including global allowed port ranges */
|
|
|
|
@ -61,12 +57,8 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
|
|
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
|
|
|
|
|
|
|
|
|
|
// New property for NetworkProxy integration
|
|
|
|
|
networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
|
|
|
|
|
|
|
|
|
|
// Browser optimization settings
|
|
|
|
|
browserFriendlyMode?: boolean; // Optimizes handling for browser connections
|
|
|
|
|
allowRenegotiationWithDifferentSNI?: boolean; // Allows SNI changes during renegotiation
|
|
|
|
|
relatedDomainPatterns?: string[][]; // Patterns for domains that should be allowed to share connections
|
|
|
|
|
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
|
|
|
|
|
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -102,11 +94,10 @@ interface IConnectionRecord {
|
|
|
|
|
incomingTerminationReason?: string | null; // Reason for incoming termination
|
|
|
|
|
outgoingTerminationReason?: string | null; // Reason for outgoing termination
|
|
|
|
|
|
|
|
|
|
// New field for NetworkProxy tracking
|
|
|
|
|
// NetworkProxy tracking
|
|
|
|
|
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
|
|
|
|
|
networkProxyIndex?: number; // Which NetworkProxy instance is being used
|
|
|
|
|
|
|
|
|
|
// New field for renegotiation handler
|
|
|
|
|
// Renegotiation handler
|
|
|
|
|
renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection
|
|
|
|
|
|
|
|
|
|
// Browser connection tracking
|
|
|
|
@ -301,35 +292,6 @@ function isClientHello(buffer: Buffer): boolean {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Checks if two domains are related based on configured patterns
|
|
|
|
|
* @param domain1 - First domain name
|
|
|
|
|
* @param domain2 - Second domain name
|
|
|
|
|
* @param relatedPatterns - Array of domain pattern groups where domains in the same group are considered related
|
|
|
|
|
* @returns true if domains are related, false otherwise
|
|
|
|
|
*/
|
|
|
|
|
function areDomainsRelated(
|
|
|
|
|
domain1: string,
|
|
|
|
|
domain2: string,
|
|
|
|
|
relatedPatterns?: string[][]
|
|
|
|
|
): boolean {
|
|
|
|
|
// Only exact same domains or empty domains are automatically related
|
|
|
|
|
if (!domain1 || !domain2 || domain1 === domain2) return true;
|
|
|
|
|
|
|
|
|
|
// Check against configured related domain patterns - the ONLY source of truth
|
|
|
|
|
if (relatedPatterns && relatedPatterns.length > 0) {
|
|
|
|
|
for (const patternGroup of relatedPatterns) {
|
|
|
|
|
const domain1Matches = patternGroup.some((pattern) => plugins.minimatch(domain1, pattern));
|
|
|
|
|
const domain2Matches = patternGroup.some((pattern) => plugins.minimatch(domain2, pattern));
|
|
|
|
|
|
|
|
|
|
if (domain1Matches && domain2Matches) return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If no patterns match, domains are not related
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper: Check if a port falls within any of the given port ranges
|
|
|
|
|
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
|
|
|
|
|
return ranges.some((range) => port >= range.from && port <= range.to);
|
|
|
|
@ -413,8 +375,8 @@ export class PortProxy {
|
|
|
|
|
private connectionsByIP: Map<string, Set<string>> = new Map();
|
|
|
|
|
private connectionRateByIP: Map<string, number[]> = new Map();
|
|
|
|
|
|
|
|
|
|
// New property to store NetworkProxy instances
|
|
|
|
|
private networkProxies: NetworkProxy[] = [];
|
|
|
|
|
// NetworkProxy instance for TLS termination
|
|
|
|
|
private networkProxy: NetworkProxy | null = null;
|
|
|
|
|
|
|
|
|
|
constructor(settingsArg: IPortProxySettings) {
|
|
|
|
|
// Set reasonable defaults for all settings
|
|
|
|
@ -434,34 +396,49 @@ export class PortProxy {
|
|
|
|
|
// Socket optimization settings
|
|
|
|
|
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
|
|
|
|
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
|
|
|
|
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds (reduced for responsiveness)
|
|
|
|
|
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
|
|
|
|
|
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds
|
|
|
|
|
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB
|
|
|
|
|
|
|
|
|
|
// Feature flags
|
|
|
|
|
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
|
|
|
|
enableKeepAliveProbes:
|
|
|
|
|
settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, // Enable by default
|
|
|
|
|
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined
|
|
|
|
|
? settingsArg.enableKeepAliveProbes : true,
|
|
|
|
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
|
|
|
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
|
|
|
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
|
|
|
|
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
|
|
|
|
|
|
|
|
|
// Rate limiting defaults
|
|
|
|
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
|
|
|
|
|
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
|
|
|
|
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
|
|
|
|
|
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
|
|
|
|
|
|
|
|
|
|
// Enhanced keep-alive settings
|
|
|
|
|
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default
|
|
|
|
|
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout
|
|
|
|
|
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended',
|
|
|
|
|
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
|
|
|
|
|
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
|
|
|
|
|
|
|
|
// Browser optimization settings (new)
|
|
|
|
|
browserFriendlyMode: settingsArg.browserFriendlyMode || true, // On by default
|
|
|
|
|
allowRenegotiationWithDifferentSNI: settingsArg.allowRenegotiationWithDifferentSNI || false, // Off by default
|
|
|
|
|
relatedDomainPatterns: settingsArg.relatedDomainPatterns || [], // Empty by default
|
|
|
|
|
|
|
|
|
|
// NetworkProxy settings
|
|
|
|
|
networkProxyPort: settingsArg.networkProxyPort || 8443, // Default NetworkProxy port
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Store NetworkProxy instances if provided
|
|
|
|
|
this.networkProxies = settingsArg.networkProxies || [];
|
|
|
|
|
// Initialize NetworkProxy if enabled
|
|
|
|
|
if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
|
|
|
|
|
this.initializeNetworkProxy();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize NetworkProxy instance
|
|
|
|
|
*/
|
|
|
|
|
private initializeNetworkProxy(): void {
|
|
|
|
|
if (!this.networkProxy) {
|
|
|
|
|
this.networkProxy = new NetworkProxy({
|
|
|
|
|
port: this.settings.networkProxyPort!,
|
|
|
|
|
portProxyIntegration: true,
|
|
|
|
|
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -469,45 +446,36 @@ export class PortProxy {
|
|
|
|
|
* @param connectionId - Unique connection identifier
|
|
|
|
|
* @param socket - The incoming client socket
|
|
|
|
|
* @param record - The connection record
|
|
|
|
|
* @param domainConfig - The domain configuration
|
|
|
|
|
* @param initialData - Initial data chunk (TLS ClientHello)
|
|
|
|
|
* @param serverName - SNI hostname (if available)
|
|
|
|
|
*/
|
|
|
|
|
private forwardToNetworkProxy(
|
|
|
|
|
connectionId: string,
|
|
|
|
|
socket: plugins.net.Socket,
|
|
|
|
|
record: IConnectionRecord,
|
|
|
|
|
domainConfig: IDomainConfig,
|
|
|
|
|
initialData: Buffer,
|
|
|
|
|
serverName?: string
|
|
|
|
|
initialData: Buffer
|
|
|
|
|
): void {
|
|
|
|
|
// Determine which NetworkProxy to use
|
|
|
|
|
const proxyIndex =
|
|
|
|
|
domainConfig.networkProxyIndex !== undefined ? domainConfig.networkProxyIndex : 0;
|
|
|
|
|
|
|
|
|
|
// Validate the NetworkProxy index
|
|
|
|
|
if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) {
|
|
|
|
|
// Ensure NetworkProxy is initialized
|
|
|
|
|
if (!this.networkProxy) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`
|
|
|
|
|
`[${connectionId}] NetworkProxy not initialized. Using fallback direct connection.`
|
|
|
|
|
);
|
|
|
|
|
// Fall back to direct connection
|
|
|
|
|
return this.setupDirectConnection(
|
|
|
|
|
connectionId,
|
|
|
|
|
socket,
|
|
|
|
|
record,
|
|
|
|
|
domainConfig,
|
|
|
|
|
serverName,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
initialData
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const networkProxy = this.networkProxies[proxyIndex];
|
|
|
|
|
const proxyPort = networkProxy.getListeningPort();
|
|
|
|
|
const proxyPort = this.networkProxy.getListeningPort();
|
|
|
|
|
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
|
|
|
|
|
`[${connectionId}] Forwarding TLS connection to NetworkProxy at ${proxyHost}:${proxyPort}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -521,7 +489,6 @@ export class PortProxy {
|
|
|
|
|
record.outgoing = proxySocket;
|
|
|
|
|
record.outgoingStartTime = Date.now();
|
|
|
|
|
record.usingNetworkProxy = true;
|
|
|
|
|
record.networkProxyIndex = proxyIndex;
|
|
|
|
|
|
|
|
|
|
// Set up error handlers
|
|
|
|
|
proxySocket.on('error', (err) => {
|
|
|
|
@ -565,7 +532,7 @@ export class PortProxy {
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
|
|
|
|
|
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
@ -886,11 +853,11 @@ export class PortProxy {
|
|
|
|
|
record.pendingData = [];
|
|
|
|
|
record.pendingDataSize = 0;
|
|
|
|
|
|
|
|
|
|
// Add the renegotiation handler for SNI validation, with browser-friendly improvements
|
|
|
|
|
// Add the renegotiation handler for SNI validation with strict domain enforcement
|
|
|
|
|
if (serverName) {
|
|
|
|
|
// Define a handler for checking renegotiation with improved detection
|
|
|
|
|
const renegotiationHandler = (renegChunk: Buffer) => {
|
|
|
|
|
// Only process if this looks like a TLS ClientHello (more precise than just checking for type 22)
|
|
|
|
|
// Only process if this looks like a TLS ClientHello
|
|
|
|
|
if (isClientHello(renegChunk)) {
|
|
|
|
|
try {
|
|
|
|
|
// Extract SNI from ClientHello
|
|
|
|
@ -899,44 +866,14 @@ export class PortProxy {
|
|
|
|
|
// Skip if no SNI was found
|
|
|
|
|
if (!newSNI) return;
|
|
|
|
|
|
|
|
|
|
// Handle SNI change during renegotiation
|
|
|
|
|
// Handle SNI change during renegotiation - always terminate for domain switches
|
|
|
|
|
if (newSNI !== record.lockedDomain) {
|
|
|
|
|
// Track domain switches for browser connections
|
|
|
|
|
if (!record.domainSwitches) record.domainSwitches = 0;
|
|
|
|
|
record.domainSwitches++;
|
|
|
|
|
|
|
|
|
|
// Check if this is a normal behavior of browser connection reuse
|
|
|
|
|
const isRelatedDomain = areDomainsRelated(
|
|
|
|
|
newSNI,
|
|
|
|
|
record.lockedDomain || '',
|
|
|
|
|
this.settings.relatedDomainPatterns
|
|
|
|
|
// Log and terminate the connection for any SNI change
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
|
|
|
|
|
`Terminating connection - SNI domain switching is not allowed.`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Decide how to handle the SNI change based on settings
|
|
|
|
|
if (this.settings.browserFriendlyMode && isRelatedDomain) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Browser domain switch detected: ${record.lockedDomain} -> ${newSNI}. ` +
|
|
|
|
|
`Domains are related, allowing connection to continue (domain switch #${record.domainSwitches}).`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Update the locked domain to the new one
|
|
|
|
|
record.lockedDomain = newSNI;
|
|
|
|
|
} else if (this.settings.allowRenegotiationWithDifferentSNI) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
|
|
|
|
|
`Allowing due to allowRenegotiationWithDifferentSNI setting.`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Update the locked domain to the new one
|
|
|
|
|
record.lockedDomain = newSNI;
|
|
|
|
|
} else {
|
|
|
|
|
// Standard strict behavior - terminate connection on SNI mismatch
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
|
|
|
|
|
`Terminating connection. Enable browserFriendlyMode to allow this.`
|
|
|
|
|
);
|
|
|
|
|
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
|
|
|
}
|
|
|
|
|
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
|
|
|
} else if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
|
|
|
|
@ -1201,7 +1138,7 @@ export class PortProxy {
|
|
|
|
|
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
|
|
|
|
record.hasKeepAlive ? 'Yes' : 'No'
|
|
|
|
|
}` +
|
|
|
|
|
`${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}` +
|
|
|
|
|
`${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` +
|
|
|
|
|
`${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
@ -1341,6 +1278,12 @@ export class PortProxy {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start NetworkProxy if configured
|
|
|
|
|
if (this.networkProxy) {
|
|
|
|
|
await this.networkProxy.start();
|
|
|
|
|
console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Define a unified connection handler for all listening ports.
|
|
|
|
|
const connectionHandler = (socket: plugins.net.Socket) => {
|
|
|
|
|
if (this.isShuttingDown) {
|
|
|
|
@ -1401,12 +1344,12 @@ export class PortProxy {
|
|
|
|
|
incomingTerminationReason: null,
|
|
|
|
|
outgoingTerminationReason: null,
|
|
|
|
|
|
|
|
|
|
// Initialize NetworkProxy tracking fields
|
|
|
|
|
// Initialize NetworkProxy tracking
|
|
|
|
|
usingNetworkProxy: false,
|
|
|
|
|
|
|
|
|
|
// Initialize browser connection tracking
|
|
|
|
|
isBrowserConnection: this.settings.browserFriendlyMode, // Assume browser if browserFriendlyMode is enabled
|
|
|
|
|
domainSwitches: 0, // Track domain switches
|
|
|
|
|
isBrowserConnection: false,
|
|
|
|
|
domainSwitches: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Apply keep-alive settings if enabled
|
|
|
|
@ -1443,7 +1386,6 @@ export class PortProxy {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
|
|
|
|
|
`Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
|
|
|
|
|
`Mode: ${this.settings.browserFriendlyMode ? 'Browser-friendly' : 'Standard'}. ` +
|
|
|
|
|
`Active connections: ${this.connectionRecords.size}`
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
@ -1452,23 +1394,16 @@ export class PortProxy {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let initialDataReceived = false;
|
|
|
|
|
// Check if this connection should be forwarded directly to NetworkProxy based on port
|
|
|
|
|
const shouldUseNetworkProxy = this.settings.useNetworkProxy &&
|
|
|
|
|
this.settings.useNetworkProxy.includes(localPort);
|
|
|
|
|
|
|
|
|
|
// Define helpers for rejecting connections
|
|
|
|
|
const rejectIncomingConnection = (reason: string, logMessage: string) => {
|
|
|
|
|
console.log(`[${connectionId}] ${logMessage}`);
|
|
|
|
|
socket.end();
|
|
|
|
|
if (connectionRecord.incomingTerminationReason === null) {
|
|
|
|
|
connectionRecord.incomingTerminationReason = reason;
|
|
|
|
|
this.incrementTerminationStat('incoming', reason);
|
|
|
|
|
}
|
|
|
|
|
this.cleanupConnection(connectionRecord, reason);
|
|
|
|
|
};
|
|
|
|
|
if (shouldUseNetworkProxy) {
|
|
|
|
|
// For NetworkProxy ports, we want to capture the TLS handshake and forward directly
|
|
|
|
|
let initialDataReceived = false;
|
|
|
|
|
|
|
|
|
|
// Set an initial timeout for SNI data if needed
|
|
|
|
|
let initialTimeout: NodeJS.Timeout | null = null;
|
|
|
|
|
if (this.settings.sniEnabled) {
|
|
|
|
|
initialTimeout = setTimeout(() => {
|
|
|
|
|
// Set an initial timeout for handshake data
|
|
|
|
|
let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
|
|
|
|
|
if (!initialDataReceived) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
|
|
|
|
@ -1486,283 +1421,331 @@ export class PortProxy {
|
|
|
|
|
if (initialTimeout.unref) {
|
|
|
|
|
initialTimeout.unref();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
connectionRecord.hasReceivedInitialData = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
socket.on('error', this.handleError('incoming', connectionRecord));
|
|
|
|
|
|
|
|
|
|
// Track data for bytes counting
|
|
|
|
|
socket.on('data', (chunk: Buffer) => {
|
|
|
|
|
connectionRecord.bytesReceived += chunk.length;
|
|
|
|
|
this.updateActivity(connectionRecord);
|
|
|
|
|
|
|
|
|
|
// Check for TLS handshake if this is the first chunk
|
|
|
|
|
if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
|
|
|
|
|
connectionRecord.isTLS = true;
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableTlsDebugLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`
|
|
|
|
|
);
|
|
|
|
|
// Try to extract SNI and log detailed debug info
|
|
|
|
|
extractSNI(chunk, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sets up the connection to the target host or NetworkProxy.
|
|
|
|
|
* @param serverName - The SNI hostname (unused when forcedDomain is provided).
|
|
|
|
|
* @param initialChunk - Optional initial data chunk.
|
|
|
|
|
* @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
|
|
|
|
|
* @param overridePort - If provided, use this port for the outgoing connection.
|
|
|
|
|
*/
|
|
|
|
|
const setupConnection = (
|
|
|
|
|
serverName: string,
|
|
|
|
|
initialChunk?: Buffer,
|
|
|
|
|
forcedDomain?: IDomainConfig,
|
|
|
|
|
overridePort?: number
|
|
|
|
|
) => {
|
|
|
|
|
// Clear the initial timeout since we've received data
|
|
|
|
|
if (initialTimeout) {
|
|
|
|
|
clearTimeout(initialTimeout);
|
|
|
|
|
initialTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark that we've received initial data
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
connectionRecord.hasReceivedInitialData = true;
|
|
|
|
|
|
|
|
|
|
// Check if this looks like a TLS handshake
|
|
|
|
|
const isTlsHandshakeDetected = initialChunk && isTlsHandshake(initialChunk);
|
|
|
|
|
if (isTlsHandshakeDetected) {
|
|
|
|
|
connectionRecord.isTLS = true;
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableTlsDebugLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
|
|
|
|
|
const domainConfig = forcedDomain
|
|
|
|
|
? forcedDomain
|
|
|
|
|
: serverName
|
|
|
|
|
? this.settings.domainConfigs.find((config) =>
|
|
|
|
|
config.domains.some((d) => plugins.minimatch(serverName, d))
|
|
|
|
|
)
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
// Save domain config in connection record
|
|
|
|
|
connectionRecord.domainConfig = domainConfig;
|
|
|
|
|
|
|
|
|
|
// IP validation is skipped if allowedIPs is empty
|
|
|
|
|
if (domainConfig) {
|
|
|
|
|
const effectiveAllowedIPs: string[] = [
|
|
|
|
|
...domainConfig.allowedIPs,
|
|
|
|
|
...(this.settings.defaultAllowedIPs || []),
|
|
|
|
|
];
|
|
|
|
|
const effectiveBlockedIPs: string[] = [
|
|
|
|
|
...(domainConfig.blockedIPs || []),
|
|
|
|
|
...(this.settings.defaultBlockedIPs || []),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Skip IP validation if allowedIPs is empty
|
|
|
|
|
if (
|
|
|
|
|
domainConfig.allowedIPs.length > 0 &&
|
|
|
|
|
!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)
|
|
|
|
|
) {
|
|
|
|
|
return rejectIncomingConnection(
|
|
|
|
|
'rejected',
|
|
|
|
|
`Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(
|
|
|
|
|
', '
|
|
|
|
|
)}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if we should forward this to a NetworkProxy
|
|
|
|
|
if (
|
|
|
|
|
isTlsHandshakeDetected &&
|
|
|
|
|
domainConfig.useNetworkProxy === true &&
|
|
|
|
|
initialChunk &&
|
|
|
|
|
this.networkProxies.length > 0
|
|
|
|
|
) {
|
|
|
|
|
return this.forwardToNetworkProxy(
|
|
|
|
|
connectionId,
|
|
|
|
|
socket,
|
|
|
|
|
connectionRecord,
|
|
|
|
|
domainConfig,
|
|
|
|
|
initialChunk,
|
|
|
|
|
serverName
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
|
|
|
|
if (
|
|
|
|
|
!isGlobIPAllowed(
|
|
|
|
|
remoteIP,
|
|
|
|
|
this.settings.defaultAllowedIPs,
|
|
|
|
|
this.settings.defaultBlockedIPs || []
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
return rejectIncomingConnection(
|
|
|
|
|
'rejected',
|
|
|
|
|
`Connection rejected: IP ${remoteIP} not allowed by default allowed list`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save the initial SNI for browser connection management
|
|
|
|
|
if (serverName) {
|
|
|
|
|
connectionRecord.lockedDomain = serverName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we didn't forward to NetworkProxy, proceed with direct connection
|
|
|
|
|
return this.setupDirectConnection(
|
|
|
|
|
connectionId,
|
|
|
|
|
socket,
|
|
|
|
|
connectionRecord,
|
|
|
|
|
domainConfig,
|
|
|
|
|
serverName,
|
|
|
|
|
initialChunk,
|
|
|
|
|
overridePort
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- PORT RANGE-BASED HANDLING ---
|
|
|
|
|
// Only apply port-based rules if the incoming port is within one of the global port ranges.
|
|
|
|
|
if (
|
|
|
|
|
this.settings.globalPortRanges &&
|
|
|
|
|
isPortInRanges(localPort, this.settings.globalPortRanges)
|
|
|
|
|
) {
|
|
|
|
|
if (this.settings.forwardAllGlobalRanges) {
|
|
|
|
|
if (
|
|
|
|
|
this.settings.defaultAllowedIPs &&
|
|
|
|
|
this.settings.defaultAllowedIPs.length > 0 &&
|
|
|
|
|
!isAllowed(remoteIP, this.settings.defaultAllowedIPs)
|
|
|
|
|
) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`
|
|
|
|
|
);
|
|
|
|
|
socket.end();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
setupConnection(
|
|
|
|
|
'',
|
|
|
|
|
undefined,
|
|
|
|
|
{
|
|
|
|
|
domains: ['global'],
|
|
|
|
|
allowedIPs: this.settings.defaultAllowedIPs || [],
|
|
|
|
|
blockedIPs: this.settings.defaultBlockedIPs || [],
|
|
|
|
|
targetIPs: [this.settings.targetIP!],
|
|
|
|
|
portRanges: [],
|
|
|
|
|
},
|
|
|
|
|
localPort
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
// Attempt to find a matching forced domain config based on the local port.
|
|
|
|
|
const forcedDomain = this.settings.domainConfigs.find(
|
|
|
|
|
(domain) =>
|
|
|
|
|
domain.portRanges &&
|
|
|
|
|
domain.portRanges.length > 0 &&
|
|
|
|
|
isPortInRanges(localPort, domain.portRanges)
|
|
|
|
|
);
|
|
|
|
|
if (forcedDomain) {
|
|
|
|
|
const effectiveAllowedIPs: string[] = [
|
|
|
|
|
...forcedDomain.allowedIPs,
|
|
|
|
|
...(this.settings.defaultAllowedIPs || []),
|
|
|
|
|
];
|
|
|
|
|
const effectiveBlockedIPs: string[] = [
|
|
|
|
|
...(forcedDomain.blockedIPs || []),
|
|
|
|
|
...(this.settings.defaultBlockedIPs || []),
|
|
|
|
|
];
|
|
|
|
|
if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(
|
|
|
|
|
', '
|
|
|
|
|
)} on port ${localPort}.`
|
|
|
|
|
);
|
|
|
|
|
socket.end();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(
|
|
|
|
|
', '
|
|
|
|
|
)}.`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
setupConnection('', undefined, forcedDomain, localPort);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Fall through to SNI/default handling if no forced domain config is found.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
|
|
|
|
|
if (this.settings.sniEnabled) {
|
|
|
|
|
initialDataReceived = false;
|
|
|
|
|
socket.on('error', this.handleError('incoming', connectionRecord));
|
|
|
|
|
|
|
|
|
|
// First data handler to capture initial TLS handshake for NetworkProxy
|
|
|
|
|
socket.once('data', (chunk: Buffer) => {
|
|
|
|
|
// Clear the initial timeout since we've received data
|
|
|
|
|
if (initialTimeout) {
|
|
|
|
|
clearTimeout(initialTimeout);
|
|
|
|
|
initialTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
connectionRecord.hasReceivedInitialData = true;
|
|
|
|
|
|
|
|
|
|
// Try to extract SNI
|
|
|
|
|
let serverName = '';
|
|
|
|
|
|
|
|
|
|
// Check if this looks like a TLS handshake
|
|
|
|
|
if (isTlsHandshake(chunk)) {
|
|
|
|
|
connectionRecord.isTLS = true;
|
|
|
|
|
|
|
|
|
|
// Forward directly to NetworkProxy without SNI processing
|
|
|
|
|
this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk);
|
|
|
|
|
} else {
|
|
|
|
|
// If not TLS, use normal direct connection
|
|
|
|
|
console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${localPort}`);
|
|
|
|
|
this.setupDirectConnection(connectionId, socket, connectionRecord, undefined, undefined, chunk);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
// For non-NetworkProxy ports, proceed with normal processing
|
|
|
|
|
|
|
|
|
|
// Define helpers for rejecting connections
|
|
|
|
|
const rejectIncomingConnection = (reason: string, logMessage: string) => {
|
|
|
|
|
console.log(`[${connectionId}] ${logMessage}`);
|
|
|
|
|
socket.end();
|
|
|
|
|
if (connectionRecord.incomingTerminationReason === null) {
|
|
|
|
|
connectionRecord.incomingTerminationReason = reason;
|
|
|
|
|
this.incrementTerminationStat('incoming', reason);
|
|
|
|
|
}
|
|
|
|
|
this.cleanupConnection(connectionRecord, reason);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let initialDataReceived = false;
|
|
|
|
|
|
|
|
|
|
// Set an initial timeout for SNI data if needed
|
|
|
|
|
let initialTimeout: NodeJS.Timeout | null = null;
|
|
|
|
|
if (this.settings.sniEnabled) {
|
|
|
|
|
initialTimeout = setTimeout(() => {
|
|
|
|
|
if (!initialDataReceived) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
|
|
|
|
|
);
|
|
|
|
|
if (connectionRecord.incomingTerminationReason === null) {
|
|
|
|
|
connectionRecord.incomingTerminationReason = 'initial_timeout';
|
|
|
|
|
this.incrementTerminationStat('incoming', 'initial_timeout');
|
|
|
|
|
}
|
|
|
|
|
socket.end();
|
|
|
|
|
this.cleanupConnection(connectionRecord, 'initial_timeout');
|
|
|
|
|
}
|
|
|
|
|
}, this.settings.initialDataTimeout!);
|
|
|
|
|
|
|
|
|
|
// Make sure timeout doesn't keep the process alive
|
|
|
|
|
if (initialTimeout.unref) {
|
|
|
|
|
initialTimeout.unref();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
connectionRecord.hasReceivedInitialData = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
socket.on('error', this.handleError('incoming', connectionRecord));
|
|
|
|
|
|
|
|
|
|
// Track data for bytes counting
|
|
|
|
|
socket.on('data', (chunk: Buffer) => {
|
|
|
|
|
connectionRecord.bytesReceived += chunk.length;
|
|
|
|
|
this.updateActivity(connectionRecord);
|
|
|
|
|
|
|
|
|
|
// Check for TLS handshake if this is the first chunk
|
|
|
|
|
if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
|
|
|
|
|
connectionRecord.isTLS = true;
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableTlsDebugLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
|
|
|
|
|
`[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`
|
|
|
|
|
);
|
|
|
|
|
// Try to extract SNI and log detailed debug info
|
|
|
|
|
extractSNI(chunk, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sets up the connection to the target host.
|
|
|
|
|
* @param serverName - The SNI hostname (unused when forcedDomain is provided).
|
|
|
|
|
* @param initialChunk - Optional initial data chunk.
|
|
|
|
|
* @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
|
|
|
|
|
* @param overridePort - If provided, use this port for the outgoing connection.
|
|
|
|
|
*/
|
|
|
|
|
const setupConnection = (
|
|
|
|
|
serverName: string,
|
|
|
|
|
initialChunk?: Buffer,
|
|
|
|
|
forcedDomain?: IDomainConfig,
|
|
|
|
|
overridePort?: number
|
|
|
|
|
) => {
|
|
|
|
|
// Clear the initial timeout since we've received data
|
|
|
|
|
if (initialTimeout) {
|
|
|
|
|
clearTimeout(initialTimeout);
|
|
|
|
|
initialTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark that we've received initial data
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
connectionRecord.hasReceivedInitialData = true;
|
|
|
|
|
|
|
|
|
|
// Check if this looks like a TLS handshake
|
|
|
|
|
const isTlsHandshakeDetected = initialChunk && isTlsHandshake(initialChunk);
|
|
|
|
|
if (isTlsHandshakeDetected) {
|
|
|
|
|
connectionRecord.isTLS = true;
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableTlsDebugLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
|
|
|
|
|
const domainConfig = forcedDomain
|
|
|
|
|
? forcedDomain
|
|
|
|
|
: serverName
|
|
|
|
|
? this.settings.domainConfigs.find((config) =>
|
|
|
|
|
config.domains.some((d) => plugins.minimatch(serverName, d))
|
|
|
|
|
)
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
// Save domain config in connection record
|
|
|
|
|
connectionRecord.domainConfig = domainConfig;
|
|
|
|
|
|
|
|
|
|
// IP validation is skipped if allowedIPs is empty
|
|
|
|
|
if (domainConfig) {
|
|
|
|
|
const effectiveAllowedIPs: string[] = [
|
|
|
|
|
...domainConfig.allowedIPs,
|
|
|
|
|
...(this.settings.defaultAllowedIPs || []),
|
|
|
|
|
];
|
|
|
|
|
const effectiveBlockedIPs: string[] = [
|
|
|
|
|
...(domainConfig.blockedIPs || []),
|
|
|
|
|
...(this.settings.defaultBlockedIPs || []),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Skip IP validation if allowedIPs is empty
|
|
|
|
|
if (
|
|
|
|
|
domainConfig.allowedIPs.length > 0 &&
|
|
|
|
|
!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)
|
|
|
|
|
) {
|
|
|
|
|
return rejectIncomingConnection(
|
|
|
|
|
'rejected',
|
|
|
|
|
`Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(
|
|
|
|
|
', '
|
|
|
|
|
)}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
|
|
|
|
if (
|
|
|
|
|
!isGlobIPAllowed(
|
|
|
|
|
remoteIP,
|
|
|
|
|
this.settings.defaultAllowedIPs,
|
|
|
|
|
this.settings.defaultBlockedIPs || []
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
return rejectIncomingConnection(
|
|
|
|
|
'rejected',
|
|
|
|
|
`Connection rejected: IP ${remoteIP} not allowed by default allowed list`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save the initial SNI
|
|
|
|
|
if (serverName) {
|
|
|
|
|
connectionRecord.lockedDomain = serverName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set up the direct connection
|
|
|
|
|
return this.setupDirectConnection(
|
|
|
|
|
connectionId,
|
|
|
|
|
socket,
|
|
|
|
|
connectionRecord,
|
|
|
|
|
domainConfig,
|
|
|
|
|
serverName,
|
|
|
|
|
initialChunk,
|
|
|
|
|
overridePort
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- PORT RANGE-BASED HANDLING ---
|
|
|
|
|
// Only apply port-based rules if the incoming port is within one of the global port ranges.
|
|
|
|
|
if (
|
|
|
|
|
this.settings.globalPortRanges &&
|
|
|
|
|
isPortInRanges(localPort, this.settings.globalPortRanges)
|
|
|
|
|
) {
|
|
|
|
|
if (this.settings.forwardAllGlobalRanges) {
|
|
|
|
|
if (
|
|
|
|
|
this.settings.defaultAllowedIPs &&
|
|
|
|
|
this.settings.defaultAllowedIPs.length > 0 &&
|
|
|
|
|
!isAllowed(remoteIP, this.settings.defaultAllowedIPs)
|
|
|
|
|
) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`
|
|
|
|
|
);
|
|
|
|
|
socket.end();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
setupConnection(
|
|
|
|
|
'',
|
|
|
|
|
undefined,
|
|
|
|
|
{
|
|
|
|
|
domains: ['global'],
|
|
|
|
|
allowedIPs: this.settings.defaultAllowedIPs || [],
|
|
|
|
|
blockedIPs: this.settings.defaultBlockedIPs || [],
|
|
|
|
|
targetIPs: [this.settings.targetIP!],
|
|
|
|
|
portRanges: [],
|
|
|
|
|
},
|
|
|
|
|
localPort
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
// Attempt to find a matching forced domain config based on the local port.
|
|
|
|
|
const forcedDomain = this.settings.domainConfigs.find(
|
|
|
|
|
(domain) =>
|
|
|
|
|
domain.portRanges &&
|
|
|
|
|
domain.portRanges.length > 0 &&
|
|
|
|
|
isPortInRanges(localPort, domain.portRanges)
|
|
|
|
|
);
|
|
|
|
|
if (forcedDomain) {
|
|
|
|
|
const effectiveAllowedIPs: string[] = [
|
|
|
|
|
...forcedDomain.allowedIPs,
|
|
|
|
|
...(this.settings.defaultAllowedIPs || []),
|
|
|
|
|
];
|
|
|
|
|
const effectiveBlockedIPs: string[] = [
|
|
|
|
|
...(forcedDomain.blockedIPs || []),
|
|
|
|
|
...(this.settings.defaultBlockedIPs || []),
|
|
|
|
|
];
|
|
|
|
|
if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(
|
|
|
|
|
', '
|
|
|
|
|
)} on port ${localPort}.`
|
|
|
|
|
);
|
|
|
|
|
socket.end();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(
|
|
|
|
|
', '
|
|
|
|
|
)}.`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
setupConnection('', undefined, forcedDomain, localPort);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Fall through to SNI/default handling if no forced domain config is found.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
|
|
|
|
|
if (this.settings.sniEnabled) {
|
|
|
|
|
initialDataReceived = false;
|
|
|
|
|
|
|
|
|
|
socket.once('data', (chunk: Buffer) => {
|
|
|
|
|
if (initialTimeout) {
|
|
|
|
|
clearTimeout(initialTimeout);
|
|
|
|
|
initialTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
|
|
|
|
|
// Try to extract SNI
|
|
|
|
|
let serverName = '';
|
|
|
|
|
|
|
|
|
|
if (isTlsHandshake(chunk)) {
|
|
|
|
|
connectionRecord.isTLS = true;
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableTlsDebugLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Lock the connection to the negotiated SNI.
|
|
|
|
|
connectionRecord.lockedDomain = serverName;
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Received connection from ${remoteIP} with SNI: ${
|
|
|
|
|
serverName || '(empty)'
|
|
|
|
|
}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
|
|
|
|
|
}
|
|
|
|
|
setupConnection(serverName, chunk);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
connectionRecord.hasReceivedInitialData = true;
|
|
|
|
|
|
|
|
|
|
// Lock the connection to the negotiated SNI.
|
|
|
|
|
connectionRecord.lockedDomain = serverName;
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Received connection from ${remoteIP} with SNI: ${
|
|
|
|
|
serverName || '(empty)'
|
|
|
|
|
}`
|
|
|
|
|
if (
|
|
|
|
|
this.settings.defaultAllowedIPs &&
|
|
|
|
|
this.settings.defaultAllowedIPs.length > 0 &&
|
|
|
|
|
!isAllowed(remoteIP, this.settings.defaultAllowedIPs)
|
|
|
|
|
) {
|
|
|
|
|
return rejectIncomingConnection(
|
|
|
|
|
'rejected',
|
|
|
|
|
`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setupConnection(serverName, chunk);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
connectionRecord.hasReceivedInitialData = true;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
this.settings.defaultAllowedIPs &&
|
|
|
|
|
this.settings.defaultAllowedIPs.length > 0 &&
|
|
|
|
|
!isAllowed(remoteIP, this.settings.defaultAllowedIPs)
|
|
|
|
|
) {
|
|
|
|
|
return rejectIncomingConnection(
|
|
|
|
|
'rejected',
|
|
|
|
|
`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`
|
|
|
|
|
);
|
|
|
|
|
setupConnection('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setupConnection('');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@ -1788,12 +1771,11 @@ export class PortProxy {
|
|
|
|
|
console.log(`Server Error on port ${port}: ${err.message}`);
|
|
|
|
|
});
|
|
|
|
|
server.listen(port, () => {
|
|
|
|
|
const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
|
|
|
|
|
console.log(
|
|
|
|
|
`PortProxy -> OK: Now listening on port ${port}${
|
|
|
|
|
this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''
|
|
|
|
|
}${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}${
|
|
|
|
|
this.settings.browserFriendlyMode ? ' (Browser-friendly mode enabled)' : ''
|
|
|
|
|
}`
|
|
|
|
|
this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : ''
|
|
|
|
|
}${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}`
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
this.netServers.push(server);
|
|
|
|
@ -1963,21 +1945,6 @@ export class PortProxy {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add or replace NetworkProxy instances
|
|
|
|
|
*/
|
|
|
|
|
public setNetworkProxies(networkProxies: NetworkProxy[]): void {
|
|
|
|
|
this.networkProxies = networkProxies;
|
|
|
|
|
console.log(`Updated NetworkProxy instances: ${this.networkProxies.length} proxies configured`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a list of configured NetworkProxy instances
|
|
|
|
|
*/
|
|
|
|
|
public getNetworkProxies(): NetworkProxy[] {
|
|
|
|
|
return this.networkProxies;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gracefully shut down the proxy
|
|
|
|
|
*/
|
|
|
|
@ -2069,6 +2036,16 @@ export class PortProxy {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stop NetworkProxy if it was started
|
|
|
|
|
if (this.networkProxy) {
|
|
|
|
|
try {
|
|
|
|
|
await this.networkProxy.stop();
|
|
|
|
|
console.log('NetworkProxy stopped successfully');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log(`Error stopping NetworkProxy: ${err}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear all tracking maps
|
|
|
|
|
this.connectionRecords.clear();
|
|
|
|
|
this.domainTargetIndices.clear();
|
|
|
|
@ -2084,4 +2061,4 @@ export class PortProxy {
|
|
|
|
|
|
|
|
|
|
console.log('PortProxy shutdown complete.');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|