feat(PortProxy/TLS): Add allowSessionTicket option to control TLS session ticket handling
This commit is contained in:
parent
9496dd5336
commit
9dbf6fdeb5
@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.41.0 - feat(PortProxy/TLS)
|
||||||
|
Add allowSessionTicket option to control TLS session ticket handling
|
||||||
|
|
||||||
|
- Introduce 'allowSessionTicket' flag (default true) in PortProxy settings to enable or disable TLS session resumption via session tickets.
|
||||||
|
- Update SniHandler with a new hasSessionResumption method to detect session ticket and PSK extensions in ClientHello messages.
|
||||||
|
- Force connection cleanup during renegotiation and initial handshake when allowSessionTicket is set to false and a session ticket is detected.
|
||||||
|
|
||||||
## 2025-03-11 - 3.40.0 - feat(SniHandler)
|
## 2025-03-11 - 3.40.0 - feat(SniHandler)
|
||||||
Add session cache support and tab reactivation detection to improve SNI extraction in TLS handshakes
|
Add session cache support and tab reactivation detection to improve SNI extraction in TLS handshakes
|
||||||
|
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '3.40.0',
|
version: '3.41.0',
|
||||||
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
|
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|||||||
enableDetailedLogging?: boolean; // Enable detailed connection logging
|
enableDetailedLogging?: boolean; // Enable detailed connection logging
|
||||||
enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
|
enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
|
||||||
enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
|
enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
|
||||||
|
allowSessionTicket?: boolean; // Allow TLS session ticket for reconnection (default: true)
|
||||||
|
|
||||||
// Rate limiting and security
|
// Rate limiting and security
|
||||||
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
|
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
|
||||||
@ -236,6 +237,8 @@ export class PortProxy {
|
|||||||
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
||||||
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
||||||
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
||||||
|
allowSessionTicket: settingsArg.allowSessionTicket !== undefined
|
||||||
|
? settingsArg.allowSessionTicket : true,
|
||||||
|
|
||||||
// Rate limiting defaults
|
// Rate limiting defaults
|
||||||
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
|
||||||
@ -935,6 +938,21 @@ export class PortProxy {
|
|||||||
destPort: record.incoming.localPort || 0
|
destPort: record.incoming.localPort || 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check for session tickets if allowSessionTicket is disabled
|
||||||
|
if (this.settings.allowSessionTicket === false) {
|
||||||
|
// Analyze for session resumption attempt (session ticket or PSK)
|
||||||
|
const hasSessionTicket = SniHandler.hasSessionResumption(renegChunk, this.settings.enableTlsDebugLogging);
|
||||||
|
|
||||||
|
if (hasSessionTicket) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Session ticket detected in renegotiation with allowSessionTicket=false. ` +
|
||||||
|
`Terminating connection to force new TLS handshake.`
|
||||||
|
);
|
||||||
|
this.initiateCleanupOnce(record, 'session_ticket_blocked');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newSNI = SniHandler.extractSNIWithResumptionSupport(renegChunk, connInfo, this.settings.enableTlsDebugLogging);
|
const newSNI = SniHandler.extractSNIWithResumptionSupport(renegChunk, connInfo, this.settings.enableTlsDebugLogging);
|
||||||
|
|
||||||
// Skip if no SNI was found
|
// Skip if no SNI was found
|
||||||
@ -970,6 +988,9 @@ export class PortProxy {
|
|||||||
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(`[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`);
|
console.log(`[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`);
|
||||||
|
if (this.settings.allowSessionTicket === false) {
|
||||||
|
console.log(`[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1541,6 +1562,26 @@ export class PortProxy {
|
|||||||
if (SniHandler.isTlsHandshake(chunk)) {
|
if (SniHandler.isTlsHandshake(chunk)) {
|
||||||
connectionRecord.isTLS = true;
|
connectionRecord.isTLS = true;
|
||||||
|
|
||||||
|
// Check for session tickets if allowSessionTicket is disabled
|
||||||
|
if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
|
||||||
|
// Analyze for session resumption attempt
|
||||||
|
const hasSessionTicket = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging);
|
||||||
|
|
||||||
|
if (hasSessionTicket) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Session ticket detected in initial ClientHello with allowSessionTicket=false. ` +
|
||||||
|
`Terminating connection to force new TLS handshake.`
|
||||||
|
);
|
||||||
|
if (connectionRecord.incomingTerminationReason === null) {
|
||||||
|
connectionRecord.incomingTerminationReason = 'session_ticket_blocked';
|
||||||
|
this.incrementTerminationStat('incoming', 'session_ticket_blocked');
|
||||||
|
}
|
||||||
|
socket.end();
|
||||||
|
this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try to extract SNI for domain-specific NetworkProxy handling
|
// Try to extract SNI for domain-specific NetworkProxy handling
|
||||||
const connInfo = {
|
const connInfo = {
|
||||||
sourceIp: remoteIP,
|
sourceIp: remoteIP,
|
||||||
@ -1886,6 +1927,26 @@ export class PortProxy {
|
|||||||
`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
|
`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for session tickets if allowSessionTicket is disabled
|
||||||
|
if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
|
||||||
|
// Analyze for session resumption attempt
|
||||||
|
const hasSessionTicket = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging);
|
||||||
|
|
||||||
|
if (hasSessionTicket) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Session ticket detected in initial ClientHello with allowSessionTicket=false. ` +
|
||||||
|
`Terminating connection to force new TLS handshake.`
|
||||||
|
);
|
||||||
|
if (connectionRecord.incomingTerminationReason === null) {
|
||||||
|
connectionRecord.incomingTerminationReason = 'session_ticket_blocked';
|
||||||
|
this.incrementTerminationStat('incoming', 'session_ticket_blocked');
|
||||||
|
}
|
||||||
|
socket.end();
|
||||||
|
this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create connection info object for SNI extraction
|
// Create connection info object for SNI extraction
|
||||||
const connInfo = {
|
const connInfo = {
|
||||||
|
@ -279,6 +279,118 @@ export class SniHandler {
|
|||||||
return buffer[5] === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE;
|
return buffer[5] === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a ClientHello message contains session resumption indicators
|
||||||
|
* such as session tickets or PSK (Pre-Shared Key) extensions.
|
||||||
|
*
|
||||||
|
* @param buffer - The buffer containing a ClientHello message
|
||||||
|
* @param enableLogging - Whether to enable logging
|
||||||
|
* @returns true if the ClientHello contains session resumption mechanisms
|
||||||
|
*/
|
||||||
|
public static hasSessionResumption(
|
||||||
|
buffer: Buffer,
|
||||||
|
enableLogging: boolean = false
|
||||||
|
): boolean {
|
||||||
|
const log = (message: string) => {
|
||||||
|
if (enableLogging) {
|
||||||
|
console.log(`[Session Resumption] ${message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.isClientHello(buffer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check for session ID presence first
|
||||||
|
let pos = 5 + 1 + 3 + 2; // Position after handshake type, length and client version
|
||||||
|
pos += 32; // Skip client random
|
||||||
|
|
||||||
|
if (pos + 1 > buffer.length) return false;
|
||||||
|
|
||||||
|
const sessionIdLength = buffer[pos];
|
||||||
|
let hasNonEmptySessionId = sessionIdLength > 0;
|
||||||
|
|
||||||
|
if (hasNonEmptySessionId) {
|
||||||
|
log(`Detected non-empty session ID (length: ${sessionIdLength})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue to check for extensions
|
||||||
|
pos += 1 + sessionIdLength;
|
||||||
|
|
||||||
|
// Skip cipher suites
|
||||||
|
if (pos + 2 > buffer.length) return false;
|
||||||
|
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
||||||
|
pos += 2 + cipherSuitesLength;
|
||||||
|
|
||||||
|
// Skip compression methods
|
||||||
|
if (pos + 1 > buffer.length) return false;
|
||||||
|
const compressionMethodsLength = buffer[pos];
|
||||||
|
pos += 1 + compressionMethodsLength;
|
||||||
|
|
||||||
|
// Check for extensions
|
||||||
|
if (pos + 2 > buffer.length) return false;
|
||||||
|
|
||||||
|
// Look for session resumption extensions
|
||||||
|
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
||||||
|
pos += 2;
|
||||||
|
|
||||||
|
// Extensions end position
|
||||||
|
const extensionsEnd = pos + extensionsLength;
|
||||||
|
if (extensionsEnd > buffer.length) return false;
|
||||||
|
|
||||||
|
// Track resumption indicators
|
||||||
|
let hasSessionTicket = false;
|
||||||
|
let hasPSK = false;
|
||||||
|
let hasEarlyData = false;
|
||||||
|
|
||||||
|
// Iterate through extensions
|
||||||
|
while (pos + 4 <= extensionsEnd) {
|
||||||
|
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
||||||
|
pos += 2;
|
||||||
|
|
||||||
|
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
||||||
|
pos += 2;
|
||||||
|
|
||||||
|
if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) {
|
||||||
|
log('Found session ticket extension');
|
||||||
|
hasSessionTicket = true;
|
||||||
|
|
||||||
|
// Check if session ticket has non-zero length (active ticket)
|
||||||
|
if (extensionLength > 0) {
|
||||||
|
log(`Session ticket has length ${extensionLength} - active ticket present`);
|
||||||
|
}
|
||||||
|
} else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
|
||||||
|
log('Found PSK extension (TLS 1.3 resumption mechanism)');
|
||||||
|
hasPSK = true;
|
||||||
|
} else if (extensionType === this.TLS_EARLY_DATA_EXTENSION_TYPE) {
|
||||||
|
log('Found Early Data extension (TLS 1.3 0-RTT)');
|
||||||
|
hasEarlyData = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip extension data
|
||||||
|
pos += extensionLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consider it a resumption if any resumption mechanism is present
|
||||||
|
const isResumption = hasSessionTicket || hasPSK || hasEarlyData ||
|
||||||
|
(hasNonEmptySessionId && !hasPSK); // Legacy resumption
|
||||||
|
|
||||||
|
if (isResumption) {
|
||||||
|
log('Session resumption detected: ' +
|
||||||
|
(hasSessionTicket ? 'session ticket, ' : '') +
|
||||||
|
(hasPSK ? 'PSK, ' : '') +
|
||||||
|
(hasEarlyData ? 'early data, ' : '') +
|
||||||
|
(hasNonEmptySessionId ? 'session ID' : ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
return isResumption;
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error checking for session resumption: ${error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects characteristics of a tab reactivation TLS handshake
|
* Detects characteristics of a tab reactivation TLS handshake
|
||||||
* These often have specific patterns in Chrome and other browsers
|
* These often have specific patterns in Chrome and other browsers
|
||||||
|
Loading…
x
Reference in New Issue
Block a user