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.

This commit is contained in:
2025-03-17 13:37:48 +00:00
parent 80d2f30804
commit ca6f6de798
5 changed files with 343 additions and 84 deletions

View File

@ -11,6 +11,7 @@ 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
@ -560,95 +561,125 @@ export class ConnectionHandler {
// Block ClientHello without SNI when allowSessionTicket is false
console.log(
`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` +
`Sending warning unrecognized_name alert to encourage immediate retry with SNI.`
`Sending unrecognized_name alert to encourage immediate retry with SNI 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'
);
}
// 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)
]);
// 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)
]);
const certificateExpiredAlert = Buffer.from([
0x15, // Alert record type
0x03,
0x03, // TLS 1.2 version
0x00,
0x02, // Length
0x01, // Warning alert level (1)
0x2F, // certificate_expired alert (47)
]);
try {
// Use cork/uncork to ensure the alert is sent as a single packet
// Send the alert but do NOT end the connection
// Using our new TlsAlert class for better alert management
socket.cork();
const writeSuccessful = socket.write(serverNameUnknownAlertData);
socket.write(TlsAlert.alerts.unrecognizedName);
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');
}, 5000); // 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
console.log(
`[${connectionId}] Alert sent, waiting for new ClientHello on same connection...`
);
// Remove existing data listener and wait for a new ClientHello
socket.removeAllListeners('data');
// 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();
}
// 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}`);
@ -656,6 +687,7 @@ export class ConnectionHandler {
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
}
// Return early to prevent the normal flow
return;
}
}