Compare commits

...

22 Commits

Author SHA1 Message Date
0fb5e5ea50 4.1.13
Some checks failed
Default (tags) / security (push) Successful in 29s
Default (tags) / test (push) Failing after 59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-17 13:15:12 +00:00
35f6739b3c fix(tls-handshake): Set certificate_expired TLS alert level to warning instead of fatal to allow graceful termination. 2025-03-17 13:15:12 +00:00
4634c68ea6 4.1.12
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-17 13:09:54 +00:00
e126032b61 fix(classes.pp.connectionhandler): Replace unrecognized_name alert data with certificate_expired alert in TLS handshake handling for session resumption without SNI 2025-03-17 13:09:54 +00:00
7797c799dd 4.1.11
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-17 13:00:02 +00:00
e8639e1b01 fix(connectionhandler): Increase delay before cleaning up connections when session resumption is blocked due to missing SNI, allowing more natural socket termination. 2025-03-17 13:00:02 +00:00
60a0ad106d 4.1.10
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-16 14:49:25 +00:00
a70c123007 fix(connectionhandler): Increase delay timings for TLS alert transmission in session ticket blocking to allow graceful socket termination 2025-03-16 14:49:25 +00:00
46aa7620b0 4.1.9
Some checks failed
Default (tags) / security (push) Successful in 35s
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:13:36 +00:00
f72db86e37 fix(ConnectionHandler): Replace closeNotify alert with handshake failure alert in TLS ClientHello handling to properly signal missing SNI and enforce session ticket restrictions. 2025-03-16 14:13:35 +00:00
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
efbb4335d7 4.1.5
Some checks failed
Default (tags) / security (push) Successful in 38s
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:19:37 +00:00
9dd402054d 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. 2025-03-16 13:19:37 +00:00
6c1efc1dc0 4.1.4
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-15 19:10:54 +00:00
cad0e6a2b2 fix(ConnectionHandler): Refactor ConnectionHandler code formatting for improved readability and consistency in log messages and whitespace handling 2025-03-15 19:10:54 +00:00
794e1292e5 4.1.3
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-15 18:51:50 +00:00
ee79f9ab7c 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. 2025-03-15 18:51:50 +00:00
4 changed files with 286 additions and 164 deletions

View File

@ -1,5 +1,79 @@
# Changelog # Changelog
## 2025-03-17 - 4.1.13 - fix(tls-handshake)
Set certificate_expired TLS alert level to warning instead of fatal to allow graceful termination.
- In the TLS handshake alert for certificate_expired (0x2F), changed the alert level from 0x02 (fatal) to 0x01 (warning).
- This change avoids abrupt connection termination, enabling a smoother handling of certificate expiration alerts.
## 2025-03-17 - 4.1.12 - fix(classes.pp.connectionhandler)
Replace unrecognized_name alert data with certificate_expired alert in TLS handshake handling for session resumption without SNI
- Switched the alert payload from serverNameUnknownAlertData to a new certificateExpiredAlert buffer
- Now sends a fatal certificate_expired alert (code 47) instead of a warning unrecognized_name alert
- Improves TLS error reporting and encourages immediate disconnection when a ClientHello lacks SNI and session tickets are disallowed
## 2025-03-17 - 4.1.11 - fix(connectionhandler)
Increase delay before cleaning up connections when session resumption is blocked due to missing SNI, allowing more natural socket termination.
- Changed cleanup delay in ts/classes.pp.connectionhandler.ts from 300ms to 1000ms.
- This fix ensures that sockets get sufficient time to terminate gracefully.
## 2025-03-16 - 4.1.10 - fix(connectionhandler)
Increase delay timings for TLS alert transmission in session ticket blocking to allow graceful socket termination
- Updated finishConnection: replaced immediate socket.destroy with a graceful end call
- Increased delay after successful write from 50ms to 200ms to allow alert processing
- Raised safety timeout from 250ms to 400ms when waiting for 'drain' event
## 2025-03-16 - 4.1.9 - fix(ConnectionHandler)
Replace closeNotify alert with handshake failure alert in TLS ClientHello handling to properly signal missing SNI and enforce session ticket restrictions.
- Switched alert data sent on missing SNI from closeNotifyAlert to sslHandshakeFailureAlertData.
- Ensures consistent TLS alert behavior during handshake failure.
## 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)
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.
- Send a TLS warning (unrecognized_name alert, code 112) when a ClientHello is received without SNI and session tickets are disallowed.
- Utilize socket cork/uncork to ensure the alert is sent as a single packet.
- Add a 5-second alert timeout and a subsequent 30-second grace period to allow clients to initiate a new handshake with SNI.
- Clean up and terminate the connection if no valid SNI is provided after the grace period, logging appropriate termination reasons.
## 2025-03-15 - 4.1.4 - fix(ConnectionHandler)
Refactor ConnectionHandler code formatting for improved readability and consistency in log messages and whitespace handling
- Standardized indentation and spacing in method signatures and log statements
- Aligned inline comments and string concatenations for clarity
- Minor refactoring of parameter formatting without changing functionality
## 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) ## 2025-03-15 - 4.1.2 - fix(connectionhandler)
Send proper TLS alert before terminating connections when SNI is missing and session tickets are disallowed. Send proper TLS alert before terminating connections when SNI is missing and session tickets are disallowed.

View File

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

@ -1,5 +1,9 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
import type { IConnectionRecord, IDomainConfig, IPortProxySettings } from './classes.pp.interfaces.js'; import type {
IConnectionRecord,
IDomainConfig,
IPortProxySettings,
} from './classes.pp.interfaces.js';
import { ConnectionManager } from './classes.pp.connectionmanager.js'; import { ConnectionManager } from './classes.pp.connectionmanager.js';
import { SecurityManager } from './classes.pp.securitymanager.js'; import { SecurityManager } from './classes.pp.securitymanager.js';
import { DomainConfigManager } from './classes.pp.domainconfigmanager.js'; import { DomainConfigManager } from './classes.pp.domainconfigmanager.js';
@ -73,8 +77,8 @@ export class ConnectionHandler {
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( console.log(
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` + `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
`Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` + `Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
`Active connections: ${this.connectionManager.getConnectionCount()}` `Active connections: ${this.connectionManager.getConnectionCount()}`
); );
} else { } else {
console.log( console.log(
@ -94,7 +98,10 @@ export class ConnectionHandler {
/** /**
* Handle a connection that should be forwarded to NetworkProxy * Handle a connection that should be forwarded to NetworkProxy
*/ */
private handleNetworkProxyConnection(socket: plugins.net.Socket, record: IConnectionRecord): void { private handleNetworkProxyConnection(
socket: plugins.net.Socket,
record: IConnectionRecord
): void {
const connectionId = record.id; const connectionId = record.id;
let initialDataReceived = false; let initialDataReceived = false;
@ -104,7 +111,7 @@ export class ConnectionHandler {
console.log( console.log(
`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}` `[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
); );
// Add a grace period instead of immediate termination // Add a grace period instead of immediate termination
setTimeout(() => { setTimeout(() => {
if (!initialDataReceived) { if (!initialDataReceived) {
@ -144,7 +151,7 @@ export class ConnectionHandler {
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) { if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
console.log( console.log(
`[${connectionId}] Non-TLS connection detected on port 443. ` + `[${connectionId}] Non-TLS connection detected on port 443. ` +
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.` `Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
); );
if (record.incomingTerminationReason === null) { if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'non_tls_blocked'; record.incomingTerminationReason = 'non_tls_blocked';
@ -159,8 +166,8 @@ export class ConnectionHandler {
if (this.tlsManager.isTlsHandshake(chunk)) { if (this.tlsManager.isTlsHandshake(chunk)) {
record.isTLS = true; record.isTLS = true;
// Check session tickets if they're disabled // Check for ClientHello to extract SNI - but don't enforce it for NetworkProxy
if (this.settings.allowSessionTicket === false && this.tlsManager.isClientHello(chunk)) { if (this.tlsManager.isClientHello(chunk)) {
// Create connection info for SNI extraction // Create connection info for SNI extraction
const connInfo = { const connInfo = {
sourceIp: record.remoteIP, sourceIp: record.remoteIP,
@ -169,73 +176,46 @@ export class ConnectionHandler {
destPort: socket.localPort || 0, destPort: socket.localPort || 0,
}; };
// Extract SNI for domain-specific NetworkProxy handling // Extract SNI for domain-specific NetworkProxy handling if available
const serverName = this.tlsManager.extractSNI(chunk, connInfo); const serverName = this.tlsManager.extractSNI(chunk, connInfo);
// If allowSessionTicket is false and we can't determine SNI, terminate the connection // For NetworkProxy connections, we'll allow session tickets even without SNI
if (!serverName) { // We'll only use the serverName if available to determine the specific NetworkProxy port
// Always block when allowSessionTicket is false and there's no SNI if (serverName) {
// Don't even check for session resumption - be strict // Save domain config and SNI in connection record
console.log( const domainConfig = this.domainConfigManager.findDomainConfig(serverName);
`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` + record.domainConfig = domainConfig;
`Terminating connection to force new TLS handshake with SNI.` record.lockedDomain = serverName;
);
// 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');
}
// 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;
}
// Save domain config and SNI in connection record // Use domain-specific NetworkProxy port if configured
const domainConfig = this.domainConfigManager.findDomainConfig(serverName); if (domainConfig && this.domainConfigManager.shouldUseNetworkProxy(domainConfig)) {
record.domainConfig = domainConfig; const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
record.lockedDomain = serverName;
// Use domain-specific NetworkProxy port if configured if (this.settings.enableDetailedLogging) {
if (domainConfig && this.domainConfigManager.shouldUseNetworkProxy(domainConfig)) { console.log(
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig); `[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`
);
}
if (this.settings.enableDetailedLogging) { // Forward to NetworkProxy with domain-specific port
console.log( this.networkProxyBridge.forwardToNetworkProxy(
`[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}` connectionId,
socket,
record,
chunk,
networkProxyPort,
(reason) => this.connectionManager.initiateCleanupOnce(record, reason)
); );
return;
} }
} else if (
// Forward to NetworkProxy with domain-specific port this.settings.allowSessionTicket === false &&
this.networkProxyBridge.forwardToNetworkProxy( this.settings.enableDetailedLogging
connectionId, ) {
socket, // Log that we're allowing a session resumption without SNI for NetworkProxy
record, console.log(
chunk, `[${connectionId}] Allowing session resumption without SNI for NetworkProxy forwarding`
networkProxyPort,
(reason) => this.connectionManager.initiateCleanupOnce(record, reason)
); );
return;
} }
} }
@ -250,14 +230,10 @@ export class ConnectionHandler {
); );
} else { } else {
// If not TLS, use normal direct connection // If not TLS, use normal direct connection
console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${record.localPort}`); console.log(
this.setupDirectConnection( `[${connectionId}] Non-TLS connection on NetworkProxy port ${record.localPort}`
socket,
record,
undefined,
undefined,
chunk
); );
this.setupDirectConnection(socket, record, undefined, undefined, chunk);
} }
}); });
} }
@ -290,7 +266,7 @@ export class ConnectionHandler {
console.log( console.log(
`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}` `[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
); );
// Add a grace period instead of immediate termination // Add a grace period instead of immediate termination
setTimeout(() => { setTimeout(() => {
if (!initialDataReceived) { if (!initialDataReceived) {
@ -375,14 +351,13 @@ export class ConnectionHandler {
record.domainConfig = domainConfig; record.domainConfig = domainConfig;
// Check if this domain should use NetworkProxy (domain-specific setting) // Check if this domain should use NetworkProxy (domain-specific setting)
if (domainConfig && if (
this.domainConfigManager.shouldUseNetworkProxy(domainConfig) && domainConfig &&
this.networkProxyBridge.getNetworkProxy()) { this.domainConfigManager.shouldUseNetworkProxy(domainConfig) &&
this.networkProxyBridge.getNetworkProxy()
) {
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( console.log(`[${connectionId}] Domain ${serverName} is configured to use NetworkProxy`);
`[${connectionId}] Domain ${serverName} is configured to use NetworkProxy`
);
} }
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig); const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
@ -404,23 +379,24 @@ export class ConnectionHandler {
// IP validation // IP validation
if (domainConfig) { if (domainConfig) {
const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig); const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig);
// Skip IP validation if allowedIPs is empty // Skip IP validation if allowedIPs is empty
if ( if (
domainConfig.allowedIPs.length > 0 && domainConfig.allowedIPs.length > 0 &&
!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs) !this.securityManager.isIPAuthorized(
record.remoteIP,
ipRules.allowedIPs,
ipRules.blockedIPs
)
) { ) {
return rejectIncomingConnection( return rejectIncomingConnection(
'rejected', 'rejected',
`Connection rejected: IP ${record.remoteIP} not allowed for domain ${domainConfig.domains.join( `Connection rejected: IP ${
', ' record.remoteIP
)}` } not allowed for domain ${domainConfig.domains.join(', ')}`
); );
} }
} else if ( } else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
this.settings.defaultAllowedIPs &&
this.settings.defaultAllowedIPs.length > 0
) {
if ( if (
!this.securityManager.isIPAuthorized( !this.securityManager.isIPAuthorized(
record.remoteIP, record.remoteIP,
@ -487,28 +463,36 @@ export class ConnectionHandler {
} else { } else {
// Attempt to find a matching forced domain config based on the local port. // Attempt to find a matching forced domain config based on the local port.
const forcedDomain = this.domainConfigManager.findDomainConfigForPort(localPort); const forcedDomain = this.domainConfigManager.findDomainConfigForPort(localPort);
if (forcedDomain) { if (forcedDomain) {
const ipRules = this.domainConfigManager.getEffectiveIPRules(forcedDomain); const ipRules = this.domainConfigManager.getEffectiveIPRules(forcedDomain);
if (!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)) { if (
!this.securityManager.isIPAuthorized(
record.remoteIP,
ipRules.allowedIPs,
ipRules.blockedIPs
)
) {
console.log( console.log(
`[${connectionId}] Connection from ${record.remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join( `[${connectionId}] Connection from ${
record.remoteIP
} rejected: IP not allowed for domain ${forcedDomain.domains.join(
', ' ', '
)} on port ${localPort}.` )} on port ${localPort}.`
); );
socket.end(); socket.end();
return; return;
} }
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( console.log(
`[${connectionId}] Port-based connection from ${record.remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join( `[${connectionId}] Port-based connection from ${
', ' record.remoteIP
)}.` } on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`
); );
} }
setupConnection('', undefined, forcedDomain, localPort); setupConnection('', undefined, forcedDomain, localPort);
return; return;
} }
@ -526,14 +510,14 @@ export class ConnectionHandler {
clearTimeout(initialTimeout); clearTimeout(initialTimeout);
initialTimeout = null; initialTimeout = null;
} }
initialDataReceived = true; initialDataReceived = true;
// Block non-TLS connections on port 443 // Block non-TLS connections on port 443
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) { if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
console.log( console.log(
`[${connectionId}] Non-TLS connection detected on port 443 in SNI handler. ` + `[${connectionId}] Non-TLS connection detected on port 443 in SNI handler. ` +
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.` `Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
); );
if (record.incomingTerminationReason === null) { if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'non_tls_blocked'; record.incomingTerminationReason = 'non_tls_blocked';
@ -566,45 +550,112 @@ export class ConnectionHandler {
// Extract SNI // Extract SNI
serverName = this.tlsManager.extractSNI(chunk, connInfo) || ''; serverName = this.tlsManager.extractSNI(chunk, connInfo) || '';
// If allowSessionTicket is false and this is a ClientHello with no SNI, terminate the connection // If allowSessionTicket is false and this is a ClientHello with no SNI, terminate the connection
if (this.settings.allowSessionTicket === false && if (
this.tlsManager.isClientHello(chunk) && this.settings.allowSessionTicket === false &&
!serverName) { this.tlsManager.isClientHello(chunk) &&
!serverName
// Always block ClientHello without SNI when allowSessionTicket is false ) {
// Don't even check for session resumption - be strict // 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. ` +
`Terminating connection to force new TLS handshake with SNI.` `Sending warning unrecognized_name alert to encourage immediate retry with SNI.`
); );
// Send a proper TLS alert before ending the connection // Set the termination reason first
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) { if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'session_ticket_blocked_no_sni'; record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
this.connectionManager.incrementTerminationStat('incoming', '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 // Create a warning-level alert for unrecognized_name
setTimeout(() => { // 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
socket.cork();
const writeSuccessful = socket.write(certificateExpiredAlert);
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
socket.end();
// Log the cleanup but wait for natural closure
setTimeout(() => {
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
}
} catch (err) {
// If we can't send the alert, fall back to immediate termination
console.log(`[${connectionId}] Error sending TLS alert: ${err.message}`);
socket.end(); socket.end();
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni'); this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
}, 50); }
return; return;
} }
} }
@ -653,23 +704,21 @@ export class ConnectionHandler {
overridePort?: number overridePort?: number
): void { ): void {
const connectionId = record.id; const connectionId = record.id;
// Determine target host // Determine target host
const targetHost = domainConfig const targetHost = domainConfig
? this.domainConfigManager.getTargetIP(domainConfig) ? this.domainConfigManager.getTargetIP(domainConfig)
: this.settings.targetIP!; : this.settings.targetIP!;
// Determine target port // Determine target port
const targetPort = overridePort !== undefined const targetPort = overridePort !== undefined ? overridePort : this.settings.toPort;
? overridePort
: this.settings.toPort;
// Setup connection options // Setup connection options
const connectionOptions: plugins.net.NetConnectOpts = { const connectionOptions: plugins.net.NetConnectOpts = {
host: targetHost, host: targetHost,
port: targetPort, port: targetPort,
}; };
// Preserve source IP if configured // Preserve source IP if configured
if (this.settings.preserveSourceIP) { if (this.settings.preserveSourceIP) {
connectionOptions.localAddress = record.remoteIP.replace('::ffff:', ''); connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
@ -926,18 +975,20 @@ export class ConnectionHandler {
// Process any remaining data in the queue before switching to piping // Process any remaining data in the queue before switching to piping
processDataQueue(); processDataQueue();
// Set up piping immediately // Set up piping immediately
pipingEstablished = true; pipingEstablished = true;
// Flush all pending data to target // Flush all pending data to target
if (record.pendingData.length > 0) { if (record.pendingData.length > 0) {
const combinedData = Buffer.concat(record.pendingData); const combinedData = Buffer.concat(record.pendingData);
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`); console.log(
`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`
);
} }
// Write pending data immediately // Write pending data immediately
targetSocket.write(combinedData, (err) => { targetSocket.write(combinedData, (err) => {
if (err) { if (err) {
@ -945,19 +996,19 @@ export class ConnectionHandler {
return this.connectionManager.initiateCleanupOnce(record, 'write_error'); return this.connectionManager.initiateCleanupOnce(record, 'write_error');
} }
}); });
// Clear the buffer now that we've processed it // Clear the buffer now that we've processed it
record.pendingData = []; record.pendingData = [];
record.pendingDataSize = 0; record.pendingDataSize = 0;
} }
// Setup piping in both directions without any delays // Setup piping in both directions without any delays
socket.pipe(targetSocket); socket.pipe(targetSocket);
targetSocket.pipe(socket); targetSocket.pipe(socket);
// Resume the socket to ensure data flows // Resume the socket to ensure data flows
socket.resume(); socket.resume();
// Process any data that might be queued in the interim // Process any data that might be queued in the interim
if (dataQueue.length > 0) { if (dataQueue.length > 0) {
// Write any remaining queued data directly to the target socket // Write any remaining queued data directly to the target socket
@ -968,7 +1019,7 @@ export class ConnectionHandler {
dataQueue.length = 0; dataQueue.length = 0;
queueSize = 0; queueSize = 0;
} }
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( console.log(
`[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` + `[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
@ -1033,15 +1084,12 @@ export class ConnectionHandler {
} }
// Set connection timeout // Set connection timeout
record.cleanupTimer = this.timeoutManager.setupConnectionTimeout( record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
record, console.log(
(record, reason) => { `[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.`
console.log( );
`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.` this.connectionManager.initiateCleanupOnce(record, reason);
); });
this.connectionManager.initiateCleanupOnce(record, reason);
}
);
// Mark TLS handshake as complete for TLS connections // Mark TLS handshake as complete for TLS connections
if (record.isTLS) { if (record.isTLS) {
@ -1055,4 +1103,4 @@ export class ConnectionHandler {
} }
}); });
} }
} }