diff --git a/changelog.md b/changelog.md index ef4bef5..0fdc8c5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2025-03-15 - 4.1.2 - fix(connectionhandler) +Send proper TLS alert before terminating connections when SNI is missing and session tickets are disallowed. + +- Added logic to transmit a fatal TLS alert (Handshake Failure) before closing the connection when no SNI is present with allowSessionTicket=false. +- Introduced a slight 50ms delay after sending the alert to ensure the client receives the alert properly. +- Applied these changes both for the initial ClientHello and when handling subsequent TLS data. + ## 2025-03-15 - 4.1.1 - fix(tls) Enforce strict SNI handling in TLS connections by terminating ClientHello messages lacking SNI when session tickets are disallowed and removing legacy session cache code. diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 77d5ea0..054c9eb 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '4.1.1', + version: '4.1.2', 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.' } diff --git a/ts/classes.pp.connectionhandler.ts b/ts/classes.pp.connectionhandler.ts index 07009f2..61d4c0f 100644 --- a/ts/classes.pp.connectionhandler.ts +++ b/ts/classes.pp.connectionhandler.ts @@ -174,16 +174,40 @@ export class ConnectionHandler { // 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 + // Don't even check for session resumption - be strict 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 + // This helps browsers like Chrome properly recognize the error + const alertData = Buffer.from([ + 0x15, // Alert record type + 0x03, 0x03, // TLS 1.2 version + 0x00, 0x02, // Length + 0x02, // Fatal alert level + 0x40 // Handshake failure alert + ]); + + try { + socket.write(alertData); + } catch (err) { + // Ignore write errors, we're closing anyway + } + if (record.incomingTerminationReason === null) { record.incomingTerminationReason = 'session_ticket_blocked_no_sni'; this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni'); } - socket.end(); - this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni'); + + // Add a small delay before ending to allow alert to be sent + setTimeout(() => { + socket.end(); + this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni'); + }, 50); + return; } @@ -548,32 +572,40 @@ export class ConnectionHandler { this.tlsManager.isClientHello(chunk) && !serverName) { - // Check if this is a session resumption - const resumptionInfo = this.tlsManager.handleSessionResumption( - chunk, - connectionId, - false // No SNI + // Always block ClientHello without SNI when allowSessionTicket is false + // Don't even check for session resumption - be strict + console.log( + `[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` + + `Terminating connection to force new TLS handshake with SNI.` ); - if (resumptionInfo.shouldBlock) { - console.log( - `[${connectionId}] Session resumption without SNI detected and allowSessionTicket=false. ` + - `Terminating connection to force new TLS handshake with SNI.` - ); - if (record.incomingTerminationReason === null) { - record.incomingTerminationReason = resumptionInfo.reason || 'session_ticket_blocked_no_sni'; - this.connectionManager.incrementTerminationStat( - 'incoming', - resumptionInfo.reason || 'session_ticket_blocked_no_sni' - ); - } - socket.end(); - this.connectionManager.cleanupConnection( - record, - resumptionInfo.reason || 'session_ticket_blocked_no_sni' - ); - return; + // Send a proper TLS alert before ending the connection + const alertData = Buffer.from([ + 0x15, // Alert record type + 0x03, 0x03, // TLS 1.2 version + 0x00, 0x02, // Length + 0x02, // Fatal alert level + 0x40 // Handshake failure alert + ]); + + try { + socket.write(alertData); + } catch (err) { + // Ignore write errors, we're closing anyway } + + if (record.incomingTerminationReason === null) { + record.incomingTerminationReason = 'session_ticket_blocked_no_sni'; + this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni'); + } + + // Add a small delay before ending to allow alert to be sent + setTimeout(() => { + socket.end(); + this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni'); + }, 50); + + return; } }