fix(TLS/SNI): Improve TLS session resumption handling and logging. Now, session resumption attempts are always logged with details, and connections without a proper SNI are rejected when allowSessionTicket is disabled. In addition, empty SNI extensions are explicitly treated as missing, ensuring stricter and more consistent TLS handshake validation.

This commit is contained in:
Philipp Kunz 2025-03-12 09:56:21 +00:00
parent b48b90d613
commit 1a90566622
4 changed files with 88 additions and 43 deletions

View File

@ -1,5 +1,12 @@
# Changelog
## 2025-03-12 - 3.41.3 - fix(TLS/SNI)
Improve TLS session resumption handling and logging. Now, session resumption attempts are always logged with details, and connections without a proper SNI are rejected when allowSessionTicket is disabled. In addition, empty SNI extensions are explicitly treated as missing, ensuring stricter and more consistent TLS handshake validation.
- Always log session resumption in both renegotiation and initial ClientHello processing.
- Terminate connections that attempt session resumption without SNI when allowSessionTicket is false.
- Treat empty SNI extensions as absence of SNI to improve consistency in TLS handshake processing.
## 2025-03-11 - 3.41.2 - fix(SniHandler)
Refactor hasSessionResumption to return detailed session resumption info

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '3.41.2',
version: '3.41.3',
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.'
}

View File

@ -943,23 +943,31 @@ export class PortProxy {
// Analyze for session resumption attempt (session ticket or PSK)
const resumptionInfo = SniHandler.hasSessionResumption(renegChunk, this.settings.enableTlsDebugLogging);
// Only block if there's a session ticket without SNI
if (resumptionInfo.isResumption && !resumptionInfo.hasSNI) {
if (resumptionInfo.isResumption) {
// Always log resumption attempt for easier debugging
console.log(
`[${connectionId}] Session ticket detected in renegotiation without SNI and allowSessionTicket=false. ` +
`[${connectionId}] Session resumption detected in renegotiation. ` +
`Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, allowSessionTicket: ${this.settings.allowSessionTicket}`
);
// Block if there's session resumption without SNI
if (!resumptionInfo.hasSNI) {
console.log(
`[${connectionId}] Session resumption detected in renegotiation without SNI and allowSessionTicket=false. ` +
`Terminating connection to force new TLS handshake.`
);
this.initiateCleanupOnce(record, 'session_ticket_blocked');
return;
} else if (resumptionInfo.isResumption && resumptionInfo.hasSNI) {
if (this.settings.enableTlsDebugLogging) {
} else {
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Session ticket with SNI detected in renegotiation. ` +
`[${connectionId}] Session resumption with SNI detected in renegotiation. ` +
`Allowing connection since SNI is present.`
);
}
}
}
}
const newSNI = SniHandler.extractSNIWithResumptionSupport(renegChunk, connInfo, this.settings.enableTlsDebugLogging);
@ -1575,10 +1583,17 @@ export class PortProxy {
// Analyze for session resumption attempt
const resumptionInfo = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging);
// Only block if there's a session ticket without SNI
if (resumptionInfo.isResumption && !resumptionInfo.hasSNI) {
if (resumptionInfo.isResumption) {
// Always log resumption attempt for easier debugging
console.log(
`[${connectionId}] Session ticket detected in initial ClientHello without SNI and allowSessionTicket=false. ` +
`[${connectionId}] Session resumption detected in initial ClientHello. ` +
`Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, allowSessionTicket: ${this.settings.allowSessionTicket}`
);
// Block if there's session resumption without SNI
if (!resumptionInfo.hasSNI) {
console.log(
`[${connectionId}] Session resumption detected in initial ClientHello without SNI and allowSessionTicket=false. ` +
`Terminating connection to force new TLS handshake.`
);
if (connectionRecord.incomingTerminationReason === null) {
@ -1588,15 +1603,16 @@ export class PortProxy {
socket.end();
this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
return;
} else if (resumptionInfo.isResumption && resumptionInfo.hasSNI) {
if (this.settings.enableTlsDebugLogging) {
} else {
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Session ticket with SNI detected in initial ClientHello. ` +
`[${connectionId}] Session resumption with SNI detected in initial ClientHello. ` +
`Allowing connection since SNI is present.`
);
}
}
}
}
// Try to extract SNI for domain-specific NetworkProxy handling
const connInfo = {
@ -1949,10 +1965,17 @@ export class PortProxy {
// Analyze for session resumption attempt
const resumptionInfo = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging);
// Only block if there's a session ticket without SNI
if (resumptionInfo.isResumption && !resumptionInfo.hasSNI) {
if (resumptionInfo.isResumption) {
// Always log resumption attempt for easier debugging
console.log(
`[${connectionId}] Session ticket detected in initial ClientHello without SNI and allowSessionTicket=false. ` +
`[${connectionId}] Session resumption detected in SNI handler. ` +
`Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, allowSessionTicket: ${this.settings.allowSessionTicket}`
);
// Block if there's session resumption without SNI
if (!resumptionInfo.hasSNI) {
console.log(
`[${connectionId}] Session resumption detected in SNI handler without SNI and allowSessionTicket=false. ` +
`Terminating connection to force new TLS handshake.`
);
if (connectionRecord.incomingTerminationReason === null) {
@ -1962,15 +1985,16 @@ export class PortProxy {
socket.end();
this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
return;
} else if (resumptionInfo.isResumption && resumptionInfo.hasSNI) {
if (this.settings.enableTlsDebugLogging) {
} else {
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Session ticket with SNI detected in initial ClientHello. ` +
`[${connectionId}] Session resumption with SNI detected in SNI handler. ` +
`Allowing connection since SNI is present.`
);
}
}
}
}
// Create connection info object for SNI extraction
const connInfo = {

View File

@ -410,8 +410,13 @@ export class SniHandler {
pos += 2;
if (extensionType === this.TLS_SNI_EXTENSION_TYPE) {
// Check that the SNI extension actually has content
if (extensionLength > 0) {
hasSNI = true;
log('Found SNI extension');
log('Found SNI extension with length: ' + extensionLength);
} else {
log('Found empty SNI extension, treating as no SNI');
}
break;
}
@ -438,6 +443,15 @@ export class SniHandler {
}
// Return an object with both flags
// For clarity: connections should be blocked if they have session resumption without SNI
if (isResumption) {
log(`Resumption summary - hasSNI: ${hasSNI ? 'yes' : 'no'}, resumption type: ${
hasSessionTicket ? 'session ticket, ' : ''
}${hasPSK ? 'PSK, ' : ''}${hasEarlyData ? 'early data, ' : ''}${
hasNonEmptySessionId ? 'session ID' : ''
}`);
}
return {
isResumption,
hasSNI