Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
794e1292e5 | |||
ee79f9ab7c | |||
107bc3b50b | |||
97982976c8 | |||
fe60f88746 | |||
252a987344 | |||
677d30563f | |||
9aa747b5d4 |
28
changelog.md
28
changelog.md
@ -1,5 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-03-15 - 4.1.3 - fix(connectionhandler)
|
||||
Improve handling of TLS ClientHello messages when allowSessionTicket is disabled and no SNI is provided by sending a warning alert (unrecognized_name, code 0x70) with a proper callback and delay to ensure the alert is transmitted before closing the connection.
|
||||
|
||||
- Replace the fatal alert (0x02/0x40) with a warning alert (0x01/0x70) to notify clients to send SNI.
|
||||
- Use socket.write callback to wait 100ms after sending the alert before terminating the connection.
|
||||
- Remove the previous short (50ms) delay in favor of a more reliable delay mechanism before cleanup.
|
||||
|
||||
## 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.
|
||||
|
||||
- In classes.pp.connectionhandler.ts, if allowSessionTicket is false and no SNI is extracted from a ClientHello, the connection is terminated to force a new handshake with SNI.
|
||||
- In classes.pp.snihandler.ts, removed session cache and related cleanup functions used for tab reactivation, simplifying SNI extraction logic.
|
||||
- Improved logging in TLS processing to aid in diagnosing handshake and session resumption issues.
|
||||
|
||||
## 2025-03-14 - 4.1.0 - feat(SniHandler)
|
||||
Enhance SNI extraction to support session caching and tab reactivation by adding session cache initialization, cleanup and helper methods. Update processTlsPacket to use cached SNI for session resumption and connection racing scenarios.
|
||||
|
||||
- Introduce initSessionCacheCleanup, cleanupSessionCache, createClientKey, cacheSession, and getCachedSession methods to manage SNI information.
|
||||
- Cache SNI based on client IP and client random to improve handling of fragmented ClientHello messages and tab reactivation.
|
||||
- Update processTlsPacket to leverage cached SNI when standard extraction fails, reducing redundant extraction and enhancing connection racing behavior.
|
||||
|
||||
## 2025-03-14 - 4.0.0 - BREAKING CHANGE(core)
|
||||
refactor: reorganize internal module structure to use 'classes.pp.*' modules
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.3",
|
||||
"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.",
|
||||
"main": "dist_ts/index.js",
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '4.0.0',
|
||||
version: '4.1.3',
|
||||
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.'
|
||||
}
|
||||
|
@ -172,35 +172,80 @@ export class ConnectionHandler {
|
||||
// Extract SNI for domain-specific NetworkProxy handling
|
||||
const serverName = this.tlsManager.extractSNI(chunk, connInfo);
|
||||
|
||||
if (serverName) {
|
||||
// If we got an SNI, check for domain-specific NetworkProxy settings
|
||||
const domainConfig = this.domainConfigManager.findDomainConfig(serverName);
|
||||
|
||||
// Save domain config and SNI in connection record
|
||||
record.domainConfig = domainConfig;
|
||||
record.lockedDomain = serverName;
|
||||
|
||||
// Use domain-specific NetworkProxy port if configured
|
||||
if (domainConfig && this.domainConfigManager.shouldUseNetworkProxy(domainConfig)) {
|
||||
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
|
||||
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`
|
||||
);
|
||||
}
|
||||
|
||||
// Forward to NetworkProxy with domain-specific port
|
||||
this.networkProxyBridge.forwardToNetworkProxy(
|
||||
connectionId,
|
||||
socket,
|
||||
record,
|
||||
chunk,
|
||||
networkProxyPort,
|
||||
(reason) => this.connectionManager.initiateCleanupOnce(record, reason)
|
||||
);
|
||||
return;
|
||||
// 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
|
||||
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
|
||||
// Using "unrecognized_name" (112) alert which is a warning level alert (1)
|
||||
// that encourages clients to retry with proper SNI
|
||||
const alertData = 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)
|
||||
]);
|
||||
|
||||
try {
|
||||
socket.write(alertData, () => {
|
||||
// Only close the socket after we're sure the alert was sent
|
||||
// Give the alert time to be processed by the client
|
||||
setTimeout(() => {
|
||||
socket.end();
|
||||
|
||||
// Ensure complete cleanup happens a bit later
|
||||
setTimeout(() => {
|
||||
if (!socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
}, 100);
|
||||
}, 100);
|
||||
});
|
||||
} catch (err) {
|
||||
// If we can't send the alert, fall back to immediate termination
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
}
|
||||
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
||||
this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Save domain config and SNI in connection record
|
||||
const domainConfig = this.domainConfigManager.findDomainConfig(serverName);
|
||||
record.domainConfig = domainConfig;
|
||||
record.lockedDomain = serverName;
|
||||
|
||||
// Use domain-specific NetworkProxy port if configured
|
||||
if (domainConfig && this.domainConfigManager.shouldUseNetworkProxy(domainConfig)) {
|
||||
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
|
||||
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`
|
||||
);
|
||||
}
|
||||
|
||||
// Forward to NetworkProxy with domain-specific port
|
||||
this.networkProxyBridge.forwardToNetworkProxy(
|
||||
connectionId,
|
||||
socket,
|
||||
record,
|
||||
chunk,
|
||||
networkProxyPort,
|
||||
(reason) => this.connectionManager.initiateCleanupOnce(record, reason)
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -531,6 +576,58 @@ export class ConnectionHandler {
|
||||
|
||||
// Extract SNI
|
||||
serverName = this.tlsManager.extractSNI(chunk, connInfo) || '';
|
||||
|
||||
// If allowSessionTicket is false and this is a ClientHello with no SNI, terminate the connection
|
||||
if (this.settings.allowSessionTicket === false &&
|
||||
this.tlsManager.isClientHello(chunk) &&
|
||||
!serverName) {
|
||||
|
||||
// Always block ClientHello without SNI when allowSessionTicket is false
|
||||
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
|
||||
// Using "unrecognized_name" (112) alert which is a warning level alert (1)
|
||||
// that encourages clients to retry with proper SNI
|
||||
const alertData = 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)
|
||||
]);
|
||||
|
||||
try {
|
||||
socket.write(alertData, () => {
|
||||
// Only close the socket after we're sure the alert was sent
|
||||
// Give the alert time to be processed by the client
|
||||
setTimeout(() => {
|
||||
socket.end();
|
||||
|
||||
// Ensure complete cleanup happens a bit later
|
||||
setTimeout(() => {
|
||||
if (!socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
}, 100);
|
||||
}, 100);
|
||||
});
|
||||
} catch (err) {
|
||||
// If we can't send the alert, fall back to immediate termination
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
}
|
||||
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
||||
this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Lock the connection to the negotiated SNI.
|
||||
|
@ -22,27 +22,6 @@ export class SniHandler {
|
||||
private static fragmentedBuffers: Map<string, Buffer> = new Map();
|
||||
private static fragmentTimeout: number = 1000; // ms to wait for fragments before cleanup
|
||||
|
||||
/**
|
||||
* Extract the client random value from a ClientHello message
|
||||
*
|
||||
* @param buffer - The buffer containing the ClientHello
|
||||
* @returns The 32-byte client random or undefined if extraction fails
|
||||
*/
|
||||
private static extractClientRandom(buffer: Buffer): Buffer | undefined {
|
||||
try {
|
||||
if (!this.isClientHello(buffer) || buffer.length < 46) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// In a ClientHello message, the client random starts at position 11
|
||||
// after record header (5 bytes), handshake type (1 byte),
|
||||
// handshake length (3 bytes), and client version (2 bytes)
|
||||
return buffer.slice(11, 11 + 32);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a buffer contains a TLS handshake message (record type 22)
|
||||
* @param buffer - The buffer to check
|
||||
@ -1174,7 +1153,7 @@ export class SniHandler {
|
||||
*
|
||||
* The method uses connection tracking to handle fragmented ClientHello
|
||||
* messages and various TLS 1.3 behaviors, including Chrome's connection
|
||||
* racing patterns.
|
||||
* racing patterns and tab reactivation behaviors.
|
||||
*
|
||||
* @param buffer - The buffer containing TLS data
|
||||
* @param connectionInfo - Connection metadata (IPs and ports)
|
||||
@ -1217,7 +1196,7 @@ export class SniHandler {
|
||||
|
||||
// Handle application data with cached SNI (for connection racing)
|
||||
if (this.isTlsApplicationData(buffer)) {
|
||||
// First check if explicit cachedSni was provided
|
||||
// If explicit cachedSni was provided, use it
|
||||
if (cachedSni) {
|
||||
log(`Using provided cached SNI for application data: ${cachedSni}`);
|
||||
return cachedSni;
|
||||
|
Reference in New Issue
Block a user