|
|
|
@ -1,5 +1,9 @@
|
|
|
|
|
import * as plugins from './plugins.js';
|
|
|
|
|
import type { IConnectionRecord, IDomainConfig, IPortProxySettings } from './classes.pp.interfaces.js';
|
|
|
|
|
import type {
|
|
|
|
|
IConnectionRecord,
|
|
|
|
|
IDomainConfig,
|
|
|
|
|
IPortProxySettings,
|
|
|
|
|
} from './classes.pp.interfaces.js';
|
|
|
|
|
import { ConnectionManager } from './classes.pp.connectionmanager.js';
|
|
|
|
|
import { SecurityManager } from './classes.pp.securitymanager.js';
|
|
|
|
|
import { DomainConfigManager } from './classes.pp.domainconfigmanager.js';
|
|
|
|
@ -73,8 +77,8 @@ export class ConnectionHandler {
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
|
|
|
|
|
`Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
|
|
|
|
|
`Active connections: ${this.connectionManager.getConnectionCount()}`
|
|
|
|
|
`Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
|
|
|
|
|
`Active connections: ${this.connectionManager.getConnectionCount()}`
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(
|
|
|
|
@ -94,7 +98,10 @@ export class ConnectionHandler {
|
|
|
|
|
/**
|
|
|
|
|
* Handle a connection that should be forwarded to NetworkProxy
|
|
|
|
|
*/
|
|
|
|
|
private handleNetworkProxyConnection(socket: plugins.net.Socket, record: IConnectionRecord): void {
|
|
|
|
|
private handleNetworkProxyConnection(
|
|
|
|
|
socket: plugins.net.Socket,
|
|
|
|
|
record: IConnectionRecord
|
|
|
|
|
): void {
|
|
|
|
|
const connectionId = record.id;
|
|
|
|
|
let initialDataReceived = false;
|
|
|
|
|
|
|
|
|
@ -104,7 +111,7 @@ export class ConnectionHandler {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add a grace period instead of immediate termination
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (!initialDataReceived) {
|
|
|
|
@ -144,7 +151,7 @@ export class ConnectionHandler {
|
|
|
|
|
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Non-TLS connection detected on port 443. ` +
|
|
|
|
|
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
|
|
|
|
|
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
|
|
|
|
|
);
|
|
|
|
|
if (record.incomingTerminationReason === null) {
|
|
|
|
|
record.incomingTerminationReason = 'non_tls_blocked';
|
|
|
|
@ -159,8 +166,8 @@ export class ConnectionHandler {
|
|
|
|
|
if (this.tlsManager.isTlsHandshake(chunk)) {
|
|
|
|
|
record.isTLS = true;
|
|
|
|
|
|
|
|
|
|
// Check session tickets if they're disabled
|
|
|
|
|
if (this.settings.allowSessionTicket === false && this.tlsManager.isClientHello(chunk)) {
|
|
|
|
|
// Check for ClientHello to extract SNI - but don't enforce it for NetworkProxy
|
|
|
|
|
if (this.tlsManager.isClientHello(chunk)) {
|
|
|
|
|
// Create connection info for SNI extraction
|
|
|
|
|
const connInfo = {
|
|
|
|
|
sourceIp: record.remoteIP,
|
|
|
|
@ -169,83 +176,46 @@ export class ConnectionHandler {
|
|
|
|
|
destPort: socket.localPort || 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Extract SNI for domain-specific NetworkProxy handling
|
|
|
|
|
// Extract SNI for domain-specific NetworkProxy handling if available
|
|
|
|
|
const serverName = this.tlsManager.extractSNI(chunk, connInfo);
|
|
|
|
|
|
|
|
|
|
// If allowSessionTicket is false and we can't determine SNI, terminate the connection
|
|
|
|
|
if (!serverName) {
|
|
|
|
|
// Always block when allowSessionTicket is false and there's no SNI
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` +
|
|
|
|
|
`Terminating connection to force new TLS handshake with SNI.`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Send a proper TLS alert before ending the connection
|
|
|
|
|
// Using "unrecognized_name" (112) alert which is a warning level alert (1)
|
|
|
|
|
// that encourages clients to retry with proper SNI
|
|
|
|
|
const alertData = Buffer.from([
|
|
|
|
|
0x15, // Alert record type
|
|
|
|
|
0x03, 0x03, // TLS 1.2 version
|
|
|
|
|
0x00, 0x02, // Length
|
|
|
|
|
0x01, // Warning alert level (not fatal)
|
|
|
|
|
0x70 // unrecognized_name alert (code 112)
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
socket.write(alertData, () => {
|
|
|
|
|
// Only close the socket after we're sure the alert was sent
|
|
|
|
|
// Give the alert time to be processed by the client
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
socket.end();
|
|
|
|
|
|
|
|
|
|
// Ensure complete cleanup happens a bit later
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (!socket.destroyed) {
|
|
|
|
|
socket.destroy();
|
|
|
|
|
}
|
|
|
|
|
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
|
|
|
|
}, 100);
|
|
|
|
|
}, 100);
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// If we can't send the alert, fall back to immediate termination
|
|
|
|
|
socket.end();
|
|
|
|
|
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (record.incomingTerminationReason === null) {
|
|
|
|
|
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
|
|
|
|
this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// For NetworkProxy connections, we'll allow session tickets even without SNI
|
|
|
|
|
// We'll only use the serverName if available to determine the specific NetworkProxy port
|
|
|
|
|
if (serverName) {
|
|
|
|
|
// Save domain config and SNI in connection record
|
|
|
|
|
const domainConfig = this.domainConfigManager.findDomainConfig(serverName);
|
|
|
|
|
record.domainConfig = domainConfig;
|
|
|
|
|
record.lockedDomain = serverName;
|
|
|
|
|
|
|
|
|
|
// Save domain config and SNI in connection record
|
|
|
|
|
const domainConfig = this.domainConfigManager.findDomainConfig(serverName);
|
|
|
|
|
record.domainConfig = domainConfig;
|
|
|
|
|
record.lockedDomain = serverName;
|
|
|
|
|
// Use domain-specific NetworkProxy port if configured
|
|
|
|
|
if (domainConfig && this.domainConfigManager.shouldUseNetworkProxy(domainConfig)) {
|
|
|
|
|
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
|
|
|
|
|
|
|
|
|
|
// Use domain-specific NetworkProxy port if configured
|
|
|
|
|
if (domainConfig && this.domainConfigManager.shouldUseNetworkProxy(domainConfig)) {
|
|
|
|
|
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`
|
|
|
|
|
// Forward to NetworkProxy with domain-specific port
|
|
|
|
|
this.networkProxyBridge.forwardToNetworkProxy(
|
|
|
|
|
connectionId,
|
|
|
|
|
socket,
|
|
|
|
|
record,
|
|
|
|
|
chunk,
|
|
|
|
|
networkProxyPort,
|
|
|
|
|
(reason) => this.connectionManager.initiateCleanupOnce(record, reason)
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Forward to NetworkProxy with domain-specific port
|
|
|
|
|
this.networkProxyBridge.forwardToNetworkProxy(
|
|
|
|
|
connectionId,
|
|
|
|
|
socket,
|
|
|
|
|
record,
|
|
|
|
|
chunk,
|
|
|
|
|
networkProxyPort,
|
|
|
|
|
(reason) => this.connectionManager.initiateCleanupOnce(record, reason)
|
|
|
|
|
} else if (
|
|
|
|
|
this.settings.allowSessionTicket === false &&
|
|
|
|
|
this.settings.enableDetailedLogging
|
|
|
|
|
) {
|
|
|
|
|
// Log that we're allowing a session resumption without SNI for NetworkProxy
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Allowing session resumption without SNI for NetworkProxy forwarding`
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -260,14 +230,10 @@ export class ConnectionHandler {
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// If not TLS, use normal direct connection
|
|
|
|
|
console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${record.localPort}`);
|
|
|
|
|
this.setupDirectConnection(
|
|
|
|
|
socket,
|
|
|
|
|
record,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
chunk
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Non-TLS connection on NetworkProxy port ${record.localPort}`
|
|
|
|
|
);
|
|
|
|
|
this.setupDirectConnection(socket, record, undefined, undefined, chunk);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@ -300,7 +266,7 @@ export class ConnectionHandler {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add a grace period instead of immediate termination
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (!initialDataReceived) {
|
|
|
|
@ -385,14 +351,13 @@ export class ConnectionHandler {
|
|
|
|
|
record.domainConfig = domainConfig;
|
|
|
|
|
|
|
|
|
|
// Check if this domain should use NetworkProxy (domain-specific setting)
|
|
|
|
|
if (domainConfig &&
|
|
|
|
|
this.domainConfigManager.shouldUseNetworkProxy(domainConfig) &&
|
|
|
|
|
this.networkProxyBridge.getNetworkProxy()) {
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
domainConfig &&
|
|
|
|
|
this.domainConfigManager.shouldUseNetworkProxy(domainConfig) &&
|
|
|
|
|
this.networkProxyBridge.getNetworkProxy()
|
|
|
|
|
) {
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Domain ${serverName} is configured to use NetworkProxy`
|
|
|
|
|
);
|
|
|
|
|
console.log(`[${connectionId}] Domain ${serverName} is configured to use NetworkProxy`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
|
|
|
|
@ -414,23 +379,24 @@ export class ConnectionHandler {
|
|
|
|
|
// IP validation
|
|
|
|
|
if (domainConfig) {
|
|
|
|
|
const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Skip IP validation if allowedIPs is empty
|
|
|
|
|
if (
|
|
|
|
|
domainConfig.allowedIPs.length > 0 &&
|
|
|
|
|
!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)
|
|
|
|
|
!this.securityManager.isIPAuthorized(
|
|
|
|
|
record.remoteIP,
|
|
|
|
|
ipRules.allowedIPs,
|
|
|
|
|
ipRules.blockedIPs
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
return rejectIncomingConnection(
|
|
|
|
|
'rejected',
|
|
|
|
|
`Connection rejected: IP ${record.remoteIP} not allowed for domain ${domainConfig.domains.join(
|
|
|
|
|
', '
|
|
|
|
|
)}`
|
|
|
|
|
`Connection rejected: IP ${
|
|
|
|
|
record.remoteIP
|
|
|
|
|
} not allowed for domain ${domainConfig.domains.join(', ')}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else if (
|
|
|
|
|
this.settings.defaultAllowedIPs &&
|
|
|
|
|
this.settings.defaultAllowedIPs.length > 0
|
|
|
|
|
) {
|
|
|
|
|
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
|
|
|
|
if (
|
|
|
|
|
!this.securityManager.isIPAuthorized(
|
|
|
|
|
record.remoteIP,
|
|
|
|
@ -497,28 +463,36 @@ export class ConnectionHandler {
|
|
|
|
|
} else {
|
|
|
|
|
// Attempt to find a matching forced domain config based on the local port.
|
|
|
|
|
const forcedDomain = this.domainConfigManager.findDomainConfigForPort(localPort);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (forcedDomain) {
|
|
|
|
|
const ipRules = this.domainConfigManager.getEffectiveIPRules(forcedDomain);
|
|
|
|
|
|
|
|
|
|
if (!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)) {
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!this.securityManager.isIPAuthorized(
|
|
|
|
|
record.remoteIP,
|
|
|
|
|
ipRules.allowedIPs,
|
|
|
|
|
ipRules.blockedIPs
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Connection from ${record.remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(
|
|
|
|
|
`[${connectionId}] Connection from ${
|
|
|
|
|
record.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 ${record.remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(
|
|
|
|
|
', '
|
|
|
|
|
)}.`
|
|
|
|
|
`[${connectionId}] Port-based connection from ${
|
|
|
|
|
record.remoteIP
|
|
|
|
|
} on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setupConnection('', undefined, forcedDomain, localPort);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@ -536,14 +510,14 @@ export class ConnectionHandler {
|
|
|
|
|
clearTimeout(initialTimeout);
|
|
|
|
|
initialTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Block non-TLS connections on port 443
|
|
|
|
|
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Non-TLS connection detected on port 443 in SNI handler. ` +
|
|
|
|
|
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
|
|
|
|
|
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
|
|
|
|
|
);
|
|
|
|
|
if (record.incomingTerminationReason === null) {
|
|
|
|
|
record.incomingTerminationReason = 'non_tls_blocked';
|
|
|
|
@ -576,42 +550,48 @@ export class ConnectionHandler {
|
|
|
|
|
|
|
|
|
|
// Extract SNI
|
|
|
|
|
serverName = this.tlsManager.extractSNI(chunk, connInfo) || '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// If allowSessionTicket is false and this is a ClientHello with no SNI, terminate the connection
|
|
|
|
|
if (this.settings.allowSessionTicket === false &&
|
|
|
|
|
this.tlsManager.isClientHello(chunk) &&
|
|
|
|
|
!serverName) {
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
this.settings.allowSessionTicket === false &&
|
|
|
|
|
this.tlsManager.isClientHello(chunk) &&
|
|
|
|
|
!serverName
|
|
|
|
|
) {
|
|
|
|
|
// Always block ClientHello without SNI when allowSessionTicket is false
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` +
|
|
|
|
|
`Terminating connection to force new TLS handshake with SNI.`
|
|
|
|
|
`Terminating connection to force new TLS handshake with SNI.`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Send a proper TLS alert before ending the connection
|
|
|
|
|
// Using "unrecognized_name" (112) alert which is a warning level alert (1)
|
|
|
|
|
// Using "unrecognized_name" (112) alert which is a warning level alert (1)
|
|
|
|
|
// that encourages clients to retry with proper SNI
|
|
|
|
|
const alertData = Buffer.from([
|
|
|
|
|
0x15, // Alert record type
|
|
|
|
|
0x03, 0x03, // TLS 1.2 version
|
|
|
|
|
0x00, 0x02, // Length
|
|
|
|
|
0x01, // Warning alert level (not fatal)
|
|
|
|
|
0x70 // unrecognized_name alert (code 112)
|
|
|
|
|
0x15, // Alert record type
|
|
|
|
|
0x03,
|
|
|
|
|
0x03, // TLS 1.2 version
|
|
|
|
|
0x00,
|
|
|
|
|
0x02, // Length
|
|
|
|
|
0x01, // Warning alert level (not fatal)
|
|
|
|
|
0x70, // unrecognized_name alert (code 112)
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
socket.write(alertData, () => {
|
|
|
|
|
// Only close the socket after we're sure the alert was sent
|
|
|
|
|
// Give the alert time to be processed by the client
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
socket.end();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure complete cleanup happens a bit later
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (!socket.destroyed) {
|
|
|
|
|
socket.destroy();
|
|
|
|
|
}
|
|
|
|
|
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
|
|
|
|
this.connectionManager.cleanupConnection(
|
|
|
|
|
record,
|
|
|
|
|
'session_ticket_blocked_no_sni'
|
|
|
|
|
);
|
|
|
|
|
}, 100);
|
|
|
|
|
}, 100);
|
|
|
|
|
});
|
|
|
|
@ -620,12 +600,15 @@ export class ConnectionHandler {
|
|
|
|
|
socket.end();
|
|
|
|
|
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (record.incomingTerminationReason === null) {
|
|
|
|
|
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
|
|
|
|
this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni');
|
|
|
|
|
this.connectionManager.incrementTerminationStat(
|
|
|
|
|
'incoming',
|
|
|
|
|
'session_ticket_blocked_no_sni'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -674,23 +657,21 @@ export class ConnectionHandler {
|
|
|
|
|
overridePort?: number
|
|
|
|
|
): void {
|
|
|
|
|
const connectionId = record.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Determine target host
|
|
|
|
|
const targetHost = domainConfig
|
|
|
|
|
? this.domainConfigManager.getTargetIP(domainConfig)
|
|
|
|
|
const targetHost = domainConfig
|
|
|
|
|
? this.domainConfigManager.getTargetIP(domainConfig)
|
|
|
|
|
: this.settings.targetIP!;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Determine target port
|
|
|
|
|
const targetPort = overridePort !== undefined
|
|
|
|
|
? overridePort
|
|
|
|
|
: this.settings.toPort;
|
|
|
|
|
|
|
|
|
|
const targetPort = overridePort !== undefined ? overridePort : this.settings.toPort;
|
|
|
|
|
|
|
|
|
|
// Setup connection options
|
|
|
|
|
const connectionOptions: plugins.net.NetConnectOpts = {
|
|
|
|
|
host: targetHost,
|
|
|
|
|
port: targetPort,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Preserve source IP if configured
|
|
|
|
|
if (this.settings.preserveSourceIP) {
|
|
|
|
|
connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
|
|
|
|
@ -947,18 +928,20 @@ export class ConnectionHandler {
|
|
|
|
|
|
|
|
|
|
// Process any remaining data in the queue before switching to piping
|
|
|
|
|
processDataQueue();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Set up piping immediately
|
|
|
|
|
pipingEstablished = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Flush all pending data to target
|
|
|
|
|
if (record.pendingData.length > 0) {
|
|
|
|
|
const combinedData = Buffer.concat(record.pendingData);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`);
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Write pending data immediately
|
|
|
|
|
targetSocket.write(combinedData, (err) => {
|
|
|
|
|
if (err) {
|
|
|
|
@ -966,19 +949,19 @@ export class ConnectionHandler {
|
|
|
|
|
return this.connectionManager.initiateCleanupOnce(record, 'write_error');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Clear the buffer now that we've processed it
|
|
|
|
|
record.pendingData = [];
|
|
|
|
|
record.pendingDataSize = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Setup piping in both directions without any delays
|
|
|
|
|
socket.pipe(targetSocket);
|
|
|
|
|
targetSocket.pipe(socket);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Resume the socket to ensure data flows
|
|
|
|
|
socket.resume();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Process any data that might be queued in the interim
|
|
|
|
|
if (dataQueue.length > 0) {
|
|
|
|
|
// Write any remaining queued data directly to the target socket
|
|
|
|
@ -989,7 +972,7 @@ export class ConnectionHandler {
|
|
|
|
|
dataQueue.length = 0;
|
|
|
|
|
queueSize = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
|
|
@ -1054,15 +1037,12 @@ export class ConnectionHandler {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set connection timeout
|
|
|
|
|
record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(
|
|
|
|
|
record,
|
|
|
|
|
(record, reason) => {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.`
|
|
|
|
|
);
|
|
|
|
|
this.connectionManager.initiateCleanupOnce(record, reason);
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.`
|
|
|
|
|
);
|
|
|
|
|
this.connectionManager.initiateCleanupOnce(record, reason);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Mark TLS handshake as complete for TLS connections
|
|
|
|
|
if (record.isTLS) {
|
|
|
|
@ -1076,4 +1056,4 @@ export class ConnectionHandler {
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|