Compare commits

...

4 Commits

Author SHA1 Message Date
b401d126bc 3.41.5
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m6s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-12 10:27:26 +00:00
baaee0ad4d fix(portproxy): Enforce TLS handshake and SNI validation on port 443 by blocking non-TLS connections and terminating session resumption attempts without SNI when allowSessionTicket is disabled. 2025-03-12 10:27:25 +00:00
fe7c4c2f5e 3.41.4
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-12 10:01:54 +00:00
ab1ec84832 fix(tls/sni): Improve logging for TLS session resumption by extracting and logging SNI values from ClientHello messages. 2025-03-12 10:01:54 +00:00
5 changed files with 127 additions and 31 deletions

View File

@ -1,5 +1,19 @@
# Changelog # Changelog
## 2025-03-12 - 3.41.5 - fix(portproxy)
Enforce TLS handshake and SNI validation on port 443 by blocking non-TLS connections and terminating session resumption attempts without SNI when allowSessionTicket is disabled.
- Added explicit check to block non-TLS connections on port 443 to ensure proper TLS usage.
- Enhanced logging for TLS ClientHello to include details on SNI extraction and session resumption status.
- Terminate connections with missing SNI by setting termination reasons ('session_ticket_blocked' or 'no_sni_blocked').
- Ensured consistent rejection of non-TLS handshakes on standard HTTPS port.
## 2025-03-12 - 3.41.4 - fix(tls/sni)
Improve logging for TLS session resumption by extracting and logging SNI values from ClientHello messages.
- Added logging to output the extracted SNI value during renegotiation, initial ClientHello and in the SNI handler.
- Enhanced error handling during SNI extraction to aid troubleshooting of TLS session resumption issues.
## 2025-03-12 - 3.41.3 - fix(TLS/SNI) ## 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. 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.

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartproxy", "name": "@push.rocks/smartproxy",
"version": "3.41.3", "version": "3.41.5",
"private": false, "private": false,
"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.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

View File

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

View File

@ -945,9 +945,13 @@ export class PortProxy {
if (resumptionInfo.isResumption) { if (resumptionInfo.isResumption) {
// Always log resumption attempt for easier debugging // Always log resumption attempt for easier debugging
// Try to extract SNI for logging
const extractedSNI = SniHandler.extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
console.log( console.log(
`[${connectionId}] Session resumption detected in renegotiation. ` + `[${connectionId}] Session resumption detected in renegotiation. ` +
`Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, allowSessionTicket: ${this.settings.allowSessionTicket}` `Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` +
`SNI value: ${extractedSNI || 'None'}, ` +
`allowSessionTicket: ${this.settings.allowSessionTicket}`
); );
// Block if there's session resumption without SNI // Block if there's session resumption without SNI
@ -1574,43 +1578,72 @@ export class PortProxy {
initialDataReceived = true; initialDataReceived = true;
connectionRecord.hasReceivedInitialData = true; connectionRecord.hasReceivedInitialData = true;
// Block non-TLS connections on port 443
// Always enforce TLS on standard HTTPS port
if (!SniHandler.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.`
);
if (connectionRecord.incomingTerminationReason === null) {
connectionRecord.incomingTerminationReason = 'non_tls_blocked';
this.incrementTerminationStat('incoming', 'non_tls_blocked');
}
socket.end();
this.cleanupConnection(connectionRecord, 'non_tls_blocked');
return;
}
// Check if this looks like a TLS handshake // Check if this looks like a TLS handshake
if (SniHandler.isTlsHandshake(chunk)) { if (SniHandler.isTlsHandshake(chunk)) {
connectionRecord.isTLS = true; connectionRecord.isTLS = true;
// Check for session tickets if allowSessionTicket is disabled // Check for TLS ClientHello with either no SNI or session tickets
if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) { if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
// Extract SNI first
const extractedSNI = SniHandler.extractSNI(chunk, this.settings.enableTlsDebugLogging);
const hasSNI = !!extractedSNI;
// Analyze for session resumption attempt // Analyze for session resumption attempt
const resumptionInfo = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging); const resumptionInfo = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging);
if (resumptionInfo.isResumption) { // Always log for debugging purposes
// Always log resumption attempt for easier debugging console.log(
console.log( `[${connectionId}] TLS ClientHello detected with allowSessionTicket=false. ` +
`[${connectionId}] Session resumption detected in initial ClientHello. ` + `Has SNI: ${hasSNI ? 'Yes' : 'No'}, ` +
`Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, allowSessionTicket: ${this.settings.allowSessionTicket}` `SNI value: ${extractedSNI || 'None'}, ` +
); `Has session resumption: ${resumptionInfo.isResumption ? 'Yes' : 'No'}`
);
// Block if there's session resumption without SNI // Block if this is a connection with session resumption but no SNI
if (!resumptionInfo.hasSNI) { if (resumptionInfo.isResumption && !hasSNI) {
console.log( console.log(
`[${connectionId}] Session resumption detected in initial ClientHello without SNI and allowSessionTicket=false. ` + `[${connectionId}] Session resumption detected in initial ClientHello without SNI and allowSessionTicket=false. ` +
`Terminating connection to force new TLS handshake.` `Terminating connection to force new TLS handshake.`
); );
if (connectionRecord.incomingTerminationReason === null) { if (connectionRecord.incomingTerminationReason === null) {
connectionRecord.incomingTerminationReason = 'session_ticket_blocked'; connectionRecord.incomingTerminationReason = 'session_ticket_blocked';
this.incrementTerminationStat('incoming', 'session_ticket_blocked'); this.incrementTerminationStat('incoming', 'session_ticket_blocked');
}
socket.end();
this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
return;
} else {
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Session resumption with SNI detected in initial ClientHello. ` +
`Allowing connection since SNI is present.`
);
}
} }
socket.end();
this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
return;
}
// Also block if this is a TLS connection without SNI when allowSessionTicket is false
// This forces clients to send SNI which helps with routing
if (!hasSNI && localPort === 443) {
console.log(
`[${connectionId}] TLS ClientHello detected on port 443 without SNI and allowSessionTicket=false. ` +
`Terminating connection to force proper SNI in handshake.`
);
if (connectionRecord.incomingTerminationReason === null) {
connectionRecord.incomingTerminationReason = 'no_sni_blocked';
this.incrementTerminationStat('incoming', 'no_sni_blocked');
}
socket.end();
this.cleanupConnection(connectionRecord, 'no_sni_blocked');
return;
} }
} }
@ -1948,6 +1981,22 @@ export class PortProxy {
initialDataReceived = true; initialDataReceived = true;
// Block non-TLS connections on port 443
// Always enforce TLS on standard HTTPS port
if (!SniHandler.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.`
);
if (connectionRecord.incomingTerminationReason === null) {
connectionRecord.incomingTerminationReason = 'non_tls_blocked';
this.incrementTerminationStat('incoming', 'non_tls_blocked');
}
socket.end();
this.cleanupConnection(connectionRecord, 'non_tls_blocked');
return;
}
// Try to extract SNI // Try to extract SNI
let serverName = ''; let serverName = '';
@ -1967,9 +2016,13 @@ export class PortProxy {
if (resumptionInfo.isResumption) { if (resumptionInfo.isResumption) {
// Always log resumption attempt for easier debugging // Always log resumption attempt for easier debugging
// Try to extract SNI for logging
const extractedSNI = SniHandler.extractSNI(chunk, this.settings.enableTlsDebugLogging);
console.log( console.log(
`[${connectionId}] Session resumption detected in SNI handler. ` + `[${connectionId}] Session resumption detected in SNI handler. ` +
`Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, allowSessionTicket: ${this.settings.allowSessionTicket}` `Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` +
`SNI value: ${extractedSNI || 'None'}, ` +
`allowSessionTicket: ${this.settings.allowSessionTicket}`
); );
// Block if there's session resumption without SNI // Block if there's session resumption without SNI

View File

@ -413,7 +413,36 @@ export class SniHandler {
// Check that the SNI extension actually has content // Check that the SNI extension actually has content
if (extensionLength > 0) { if (extensionLength > 0) {
hasSNI = true; hasSNI = true;
log('Found SNI extension with length: ' + extensionLength);
// Try to extract the actual SNI value for logging
try {
// Skip to server_name_list_length (2 bytes)
const tempPos = pos;
if (tempPos + 2 <= extensionsEnd) {
const nameListLength = (buffer[tempPos] << 8) + buffer[tempPos + 1];
// Skip server_name_list_length (2 bytes)
if (tempPos + 2 + 1 <= extensionsEnd) {
// Check name_type (should be 0 for hostname)
if (buffer[tempPos + 2] === 0) {
// Skip name_type (1 byte)
if (tempPos + 3 + 2 <= extensionsEnd) {
// Get name_length (2 bytes)
const nameLength = (buffer[tempPos + 3] << 8) + buffer[tempPos + 4];
// Extract the hostname
if (tempPos + 5 + nameLength <= extensionsEnd) {
const hostname = buffer.slice(tempPos + 5, tempPos + 5 + nameLength).toString('utf8');
log(`Found SNI extension with server_name: ${hostname}`);
}
}
}
}
}
} catch (e) {
log(`Error extracting SNI value: ${e}`);
log('Found SNI extension with length: ' + extensionLength);
}
} else { } else {
log('Found empty SNI extension, treating as no SNI'); log('Found empty SNI extension, treating as no SNI');
} }