Compare commits

...

6 Commits

Author SHA1 Message Date
d612df107e 4.1.8
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-16 14:02:18 +00:00
1c34578c36 fix(ConnectionHandler/tls): Change the TLS alert sent when a ClientHello lacks SNI: use the close_notify alert instead of handshake_failure to prompt immediate retry with SNI. 2025-03-16 14:02:18 +00:00
1f9943b5a7 4.1.7
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-16 13:47:34 +00:00
67ddf97547 fix(classes.pp.connectionhandler): Improve TLS alert handling in ClientHello when SNI is missing and session tickets are disallowed 2025-03-16 13:47:34 +00:00
8a96b45ece 4.1.6
Some checks failed
Default (tags) / security (push) Successful in 29s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-16 13:28:48 +00:00
2b6464acd5 fix(tls): Refine TLS ClientHello handling when allowSessionTicket is false by replacing extensive alert timeout logic with a concise warning alert and short delay, encouraging immediate client retry with proper SNI 2025-03-16 13:28:48 +00:00
4 changed files with 83 additions and 89 deletions

View File

@ -1,5 +1,25 @@
# Changelog # Changelog
## 2025-03-16 - 4.1.8 - fix(ConnectionHandler/tls)
Change the TLS alert sent when a ClientHello lacks SNI: use the close_notify alert instead of handshake_failure to prompt immediate retry with SNI.
- Replaced the previously sent handshake_failure alert (code 0x28) with a close_notify alert (code 0x00) in the TLS session resumption handling in ConnectionHandler.
- This change encourages clients to immediately retry and include SNI when allowSessionTicket is false.
## 2025-03-16 - 4.1.7 - fix(classes.pp.connectionhandler)
Improve TLS alert handling in ClientHello when SNI is missing and session tickets are disallowed
- Replace the unrecognized_name alert with a handshake_failure alert to ensure better client behavior.
- Refactor the alert sending mechanism using cork/uncork and add a safety timeout for the drain event.
- Enhance logging for debugging TLS handshake failures when SNI is absent.
## 2025-03-16 - 4.1.6 - fix(tls)
Refine TLS ClientHello handling when allowSessionTicket is false by replacing extensive alert timeout logic with a concise warning alert and short delay, encouraging immediate client retry with proper SNI
- Update the TLS alert sending mechanism to use cork/uncork and a short, fixed delay instead of long timeouts
- Remove redundant event listeners and excessive cleanup logic after sending the alert
- Improve code clarity and encourage clients (e.g., Chrome) to retry handshake with SNI more responsively
## 2025-03-16 - 4.1.5 - fix(TLS/ConnectionHandler) ## 2025-03-16 - 4.1.5 - fix(TLS/ConnectionHandler)
Improve handling of TLS session resumption without SNI by sending an 'unrecognized_name' alert instead of immediately terminating the connection. This change adds a grace period for the client to retry the handshake with proper SNI and cleans up the connection if no valid response is received. Improve handling of TLS session resumption without SNI by sending an 'unrecognized_name' alert instead of immediately terminating the connection. This change adds a grace period for the client to retry the handshake with proper SNI and cleans up the connection if no valid response is received.

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartproxy", "name": "@push.rocks/smartproxy",
"version": "4.1.5", "version": "4.1.8",
"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: '4.1.5', version: '4.1.8',
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

@ -557,13 +557,13 @@ export class ConnectionHandler {
this.tlsManager.isClientHello(chunk) && this.tlsManager.isClientHello(chunk) &&
!serverName !serverName
) { ) {
// Always block ClientHello without SNI when allowSessionTicket is false // Block ClientHello without SNI when allowSessionTicket is false
console.log( console.log(
`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` + `[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` +
`Sending unrecognized_name alert to encourage client to retry with SNI.` `Sending warning unrecognized_name alert to encourage immediate retry with SNI.`
); );
// Set the termination reason first to avoid races // Set the termination reason first
if (record.incomingTerminationReason === null) { if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'session_ticket_blocked_no_sni'; record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
this.connectionManager.incrementTerminationStat( this.connectionManager.incrementTerminationStat(
@ -572,10 +572,9 @@ export class ConnectionHandler {
); );
} }
// Send a proper TLS alert before ending the connection // Create a warning-level alert for unrecognized_name
// Using "unrecognized_name" (112) alert which is a warning level alert (1) // This encourages Chrome to retry immediately with SNI
// that encourages clients to retry with proper SNI const serverNameUnknownAlertData = Buffer.from([
const alertData = Buffer.from([
0x15, // Alert record type 0x15, // Alert record type
0x03, 0x03,
0x03, // TLS 1.2 version 0x03, // TLS 1.2 version
@ -585,89 +584,64 @@ export class ConnectionHandler {
0x70, // unrecognized_name alert (code 112) 0x70, // unrecognized_name alert (code 112)
]); ]);
// Send a handshake_failure alert instead of unrecognized_name
const sslHandshakeFailureAlertData = Buffer.from([
0x15, // Alert record type
0x03,
0x03, // TLS 1.2 version
0x00,
0x02, // Length
0x01, // Warning alert level (not fatal)
0x28, // handshake_failure alert (40) instead of unrecognized_name (112)
]);
const closeNotifyAlert = Buffer.from([
0x15, // Alert record type
0x03,
0x03, // TLS 1.2 version
0x00,
0x02, // Length
0x01, // Warning alert level (1)
0x00, // close_notify alert (0)
]);
try { try {
// Make sure the alert is sent as a single packet // Use cork/uncork to ensure the alert is sent as a single packet
socket.cork(); socket.cork();
socket.write(alertData); const writeSuccessful = socket.write(closeNotifyAlert);
socket.uncork(); socket.uncork();
// Set a longer timeout to allow the client to properly handle the alert // Function to handle the clean socket termination
// The client might respond by closing the connection or initiating a new handshake const finishConnection = () => {
const alertTimeout = setTimeout(() => { // First call end() to initiate a graceful close (sends FIN)
if (!socket.destroyed) {
console.log(`[${connectionId}] Client didn't respond to TLS alert, closing connection gracefully.`);
// Gracefully end the connection
socket.end(); socket.end();
// Only destroy after a delay if it's still hanging // Allow a short delay for the alert and FIN to be transmitted
// before we fully close the socket
setTimeout(() => { setTimeout(() => {
if (!socket.destroyed) { if (!socket.destroyed) {
console.log(`[${connectionId}] Forcibly closing connection that didn't terminate properly.`);
socket.destroy(); socket.destroy();
} }
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni'); this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
}, 2000); }, 150); // Short delay, but longer than the standard TCP ACK timeout
} };
}, 5000); // Give the client 5 seconds to respond to our alert
// Don't let this timeout keep the process alive if (writeSuccessful) {
if (alertTimeout.unref) { // If the data was successfully written to the kernel buffer,
alertTimeout.unref(); // we can finish the connection after a short delay to ensure transmission
} setTimeout(finishConnection, 50);
} else {
// Handle a proper close from the client // If the kernel buffer was full, wait for the drain event
socket.once('close', () => { socket.once('drain', () => {
clearTimeout(alertTimeout); setTimeout(finishConnection, 50);
console.log(`[${connectionId}] Client closed connection after receiving TLS alert.`);
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni_client_closed');
}); });
// Also handle if the client sends more data (possibly a new handshake with SNI) // Set a safety timeout in case drain never happens
socket.once('data', (newChunk) => {
clearTimeout(alertTimeout);
console.log(`[${connectionId}] Client sent new data after TLS alert, checking for SNI...`);
// This would normally be handled by our renegotiation handler,
// but since we're in a special case, we'll check for SNI again
if (this.tlsManager.isTlsHandshake(newChunk) && this.tlsManager.isClientHello(newChunk)) {
const newServerName = this.tlsManager.extractSNI(newChunk, connInfo);
if (newServerName) {
console.log(`[${connectionId}] Client provided SNI in new handshake: ${newServerName}`);
// Update the record
record.incomingTerminationReason = null;
// Remove termination stats increment
// Note: This is a little hacky as we don't have a proper way to decrement stats
// Process the new handshake with SNI
record.lockedDomain = newServerName;
setupConnection(newServerName, newChunk);
return;
} else {
console.log(`[${connectionId}] Client sent new handshake but still without SNI, closing connection.`);
socket.end();
setTimeout(() => { setTimeout(() => {
if (!socket.destroyed) { socket.removeAllListeners('drain');
socket.destroy(); finishConnection();
}, 250);
} }
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni_retry_failed');
}, 500);
}
} else {
console.log(`[${connectionId}] Client sent non-handshake data after TLS alert, closing connection.`);
socket.end();
setTimeout(() => {
if (!socket.destroyed) {
socket.destroy();
}
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_invalid_response');
}, 500);
}
});
} catch (err) { } catch (err) {
// If we can't send the alert, fall back to immediate termination // If we can't send the alert, fall back to immediate termination
console.log(`[${connectionId}] Error sending TLS alert: ${err.message}`); console.log(`[${connectionId}] Error sending TLS alert: ${err.message}`);