feat(tlsalert): add sendForceSniSequence and sendFatalAndClose helper functions to TlsAlert for improved SNI enforcement
This commit is contained in:
parent
3593e411cf
commit
b55e2da23e
@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-03-17 - 4.2.0 - feat(tlsalert)
|
||||
add sendForceSniSequence and sendFatalAndClose helper functions to TlsAlert for improved SNI enforcement
|
||||
|
||||
- Introduce sendForceSniSequence to combine multiple alerts and force clients to provide SNI
|
||||
- Add sendFatalAndClose to immediately send a fatal alert and close the connection
|
||||
- Enhance TLS alert handling for better browser compatibility and error management
|
||||
|
||||
## 2025-03-17 - 4.1.16 - fix(tls)
|
||||
Improve TLS alert handling in connection handler: use the new TlsAlert class to send proper unrecognized_name alerts when a ClientHello is missing SNI and wait for a retry on the same connection before closing. Also, add alertFallbackTimeout tracking to connection records for better timeout management.
|
||||
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '4.1.16',
|
||||
version: '4.2.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.'
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import { TlsManager } from './classes.pp.tlsmanager.js';
|
||||
import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
|
||||
import { TimeoutManager } from './classes.pp.timeoutmanager.js';
|
||||
import { PortRangeManager } from './classes.pp.portrangemanager.js';
|
||||
import { TlsAlert } from './classes.pp.tlsalert.js';
|
||||
|
||||
/**
|
||||
* Handles new connection processing and setup logic
|
||||
@ -561,125 +560,64 @@ export class ConnectionHandler {
|
||||
// Block ClientHello without SNI when allowSessionTicket is false
|
||||
console.log(
|
||||
`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` +
|
||||
`Sending unrecognized_name alert to encourage immediate retry with SNI on same connection.`
|
||||
`Sending warning unrecognized_name alert to encourage immediate retry with SNI.`
|
||||
);
|
||||
|
||||
try {
|
||||
// Send the alert but do NOT end the connection
|
||||
// Using our new TlsAlert class for better alert management
|
||||
socket.cork();
|
||||
socket.write(TlsAlert.alerts.unrecognizedName);
|
||||
socket.uncork();
|
||||
|
||||
console.log(
|
||||
`[${connectionId}] Alert sent, waiting for new ClientHello on same connection...`
|
||||
// Set the termination reason first
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
||||
this.connectionManager.incrementTerminationStat(
|
||||
'incoming',
|
||||
'session_ticket_blocked_no_sni'
|
||||
);
|
||||
}
|
||||
|
||||
// Remove existing data listener and wait for a new ClientHello
|
||||
socket.removeAllListeners('data');
|
||||
// Create a warning-level alert for unrecognized_name
|
||||
// This encourages Chrome to retry immediately with SNI
|
||||
const serverNameUnknownAlertData = Buffer.from([
|
||||
0x15, // Alert record type
|
||||
0x03,
|
||||
0x03, // TLS 1.2 version
|
||||
0x00,
|
||||
0x02, // Length
|
||||
0x01, // Warning alert level (not fatal)
|
||||
0x70, // unrecognized_name alert (code 112)
|
||||
]);
|
||||
|
||||
// Set up a new data handler to capture the next message
|
||||
socket.once('data', (retryChunk) => {
|
||||
// Cancel the fallback timeout as we received data
|
||||
if (record.alertFallbackTimeout) {
|
||||
clearTimeout(record.alertFallbackTimeout);
|
||||
record.alertFallbackTimeout = null;
|
||||
}
|
||||
|
||||
// Check if this is a new ClientHello
|
||||
if (this.tlsManager.isClientHello(retryChunk)) {
|
||||
console.log(`[${connectionId}] Received new ClientHello after alert`);
|
||||
|
||||
// Extract SNI from the new ClientHello
|
||||
const newServerName = this.tlsManager.extractSNI(retryChunk, connInfo) || '';
|
||||
|
||||
if (newServerName) {
|
||||
console.log(`[${connectionId}] New ClientHello contains SNI: ${newServerName}`);
|
||||
|
||||
// Update the record with the new SNI
|
||||
record.lockedDomain = newServerName;
|
||||
|
||||
// Continue with normal connection setup using the new chunk with SNI
|
||||
setupConnection(newServerName, retryChunk);
|
||||
} else {
|
||||
console.log(
|
||||
`[${connectionId}] New ClientHello still missing SNI, closing connection`
|
||||
);
|
||||
|
||||
// If still no SNI after retry, now we can close the connection
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
||||
this.connectionManager.incrementTerminationStat(
|
||||
'incoming',
|
||||
'session_ticket_blocked_no_sni'
|
||||
);
|
||||
}
|
||||
|
||||
// Send a close_notify alert before ending the connection
|
||||
TlsAlert.sendCloseNotify(socket)
|
||||
.catch((err) => {
|
||||
console.log(`[${connectionId}] Error sending close_notify: ${err.message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
// Clean up even if sending the alert fails
|
||||
this.connectionManager.cleanupConnection(
|
||||
record,
|
||||
'session_ticket_blocked_no_sni'
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`[${connectionId}] Received non-ClientHello data after alert, closing connection`
|
||||
);
|
||||
|
||||
// If we got something other than a ClientHello, close the connection
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'invalid_protocol';
|
||||
this.connectionManager.incrementTerminationStat('incoming', 'invalid_protocol');
|
||||
}
|
||||
|
||||
// Send a protocol_version alert before ending the connection
|
||||
TlsAlert.send(socket, TlsAlert.LEVEL_FATAL, TlsAlert.PROTOCOL_VERSION, true)
|
||||
.catch((err) => {
|
||||
console.log(
|
||||
`[${connectionId}] Error sending protocol_version alert: ${err.message}`
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
// Clean up even if sending the alert fails
|
||||
this.connectionManager.cleanupConnection(record, 'invalid_protocol');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Set a fallback timeout in case the client doesn't respond
|
||||
const fallbackTimeout = setTimeout(() => {
|
||||
console.log(`[${connectionId}] No response after alert, closing connection`);
|
||||
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'alert_timeout';
|
||||
this.connectionManager.incrementTerminationStat('incoming', 'alert_timeout');
|
||||
}
|
||||
|
||||
// Send a close_notify alert before ending the connection
|
||||
TlsAlert.sendCloseNotify(socket)
|
||||
.catch((err) => {
|
||||
console.log(`[${connectionId}] Error sending close_notify: ${err.message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
// Clean up even if sending the alert fails
|
||||
this.connectionManager.cleanupConnection(record, 'alert_timeout');
|
||||
});
|
||||
}, 10000); // 10 second timeout
|
||||
|
||||
// Make sure the timeout doesn't keep the process alive
|
||||
if (fallbackTimeout.unref) {
|
||||
fallbackTimeout.unref();
|
||||
try {
|
||||
// Use cork/uncork to ensure the alert is sent as a single packet
|
||||
socket.cork();
|
||||
const writeSuccessful = socket.write(serverNameUnknownAlertData);
|
||||
socket.uncork();
|
||||
|
||||
// Function to handle the clean socket termination - but more gradually
|
||||
const finishConnection = () => {
|
||||
// Give Chrome more time to process the alert before closing
|
||||
// We won't call destroy() at all - just end() and let the socket close naturally
|
||||
|
||||
// Log the cleanup but wait for natural closure
|
||||
setTimeout(() => {
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
}, 1000); // Longer delay to let socket cleanup happen naturally
|
||||
};
|
||||
|
||||
if (writeSuccessful) {
|
||||
// Wait longer before ending connection to ensure alert is processed by client
|
||||
setTimeout(finishConnection, 200); // Increased from 50ms to 200ms
|
||||
} else {
|
||||
// If the kernel buffer was full, wait for the drain event
|
||||
socket.once('drain', () => {
|
||||
// Wait longer after drain as well
|
||||
setTimeout(finishConnection, 200);
|
||||
});
|
||||
|
||||
// Safety timeout is increased too
|
||||
setTimeout(() => {
|
||||
socket.removeAllListeners('drain');
|
||||
finishConnection();
|
||||
}, 400); // Increased from 250ms to 400ms
|
||||
}
|
||||
|
||||
// Store the timeout in the record so it can be cleared during cleanup
|
||||
record.alertFallbackTimeout = fallbackTimeout;
|
||||
} catch (err) {
|
||||
// If we can't send the alert, fall back to immediate termination
|
||||
console.log(`[${connectionId}] Error sending TLS alert: ${err.message}`);
|
||||
@ -687,7 +625,6 @@ export class ConnectionHandler {
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
}
|
||||
|
||||
// Return early to prevent the normal flow
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -173,6 +173,7 @@ export class TlsAlert {
|
||||
protocolVersion: TlsAlert.createFatal(TlsAlert.PROTOCOL_VERSION),
|
||||
insufficientSecurity: TlsAlert.createFatal(TlsAlert.INSUFFICIENT_SECURITY),
|
||||
internalError: TlsAlert.createFatal(TlsAlert.INTERNAL_ERROR),
|
||||
unrecognizedNameFatal: TlsAlert.createFatal(TlsAlert.UNRECOGNIZED_NAME),
|
||||
};
|
||||
|
||||
/**
|
||||
@ -215,4 +216,43 @@ export class TlsAlert {
|
||||
const level = fatal ? this.LEVEL_FATAL : this.LEVEL_WARNING;
|
||||
return this.send(socket, level, this.CERTIFICATE_EXPIRED, closeAfterSend, closeDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a sequence of alerts to force SNI from clients
|
||||
* This combines multiple alerts to ensure maximum browser compatibility
|
||||
*
|
||||
* @param socket The socket to send the alerts to
|
||||
* @returns Promise that resolves when all alerts have been sent
|
||||
*/
|
||||
static async sendForceSniSequence(socket: net.Socket): Promise<void> {
|
||||
try {
|
||||
// Send unrecognized_name (warning)
|
||||
socket.cork();
|
||||
socket.write(this.alerts.unrecognizedName);
|
||||
socket.uncork();
|
||||
|
||||
// Give the socket time to send the alert
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, 50);
|
||||
});
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a fatal level alert that immediately terminates the connection
|
||||
*
|
||||
* @param socket The socket to send the alert to
|
||||
* @param description Alert description code
|
||||
* @param closeDelay Milliseconds to wait before closing the connection (default: 100ms)
|
||||
* @returns Promise that resolves when the alert has been sent and the connection closed
|
||||
*/
|
||||
static async sendFatalAndClose(
|
||||
socket: net.Socket,
|
||||
description: number,
|
||||
closeDelay: number = 100
|
||||
): Promise<void> {
|
||||
return this.send(socket, this.LEVEL_FATAL, description, true, closeDelay);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user