Compare commits

...

34 Commits

Author SHA1 Message Date
b10f35be4b 4.2.2
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 1m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-18 00:29:17 +00:00
426249e70e fix(connectionhandler): Ensure proper termination of TLS connections without SNI by explicitly ending the socket after sending the unrecognized_name alert. This prevents the connection from hanging and avoids potential duplicate handling. 2025-03-18 00:29:17 +00:00
ba0d9d0b8e 4.2.1
Some checks failed
Default (tags) / security (push) Failing after 14m48s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-17 14:28:09 +00:00
151b8f498c fix(core): No uncommitted changes detected in the project. 2025-03-17 14:28:08 +00:00
0db4b07b22 4.2.0
Some checks failed
Default (tags) / security (push) Successful in 58s
Default (tags) / test (push) Failing after 14m46s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-17 14:27:10 +00:00
b55e2da23e feat(tlsalert): add sendForceSniSequence and sendFatalAndClose helper functions to TlsAlert for improved SNI enforcement 2025-03-17 14:27:10 +00:00
3593e411cf 4.1.16
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-17 13:37:48 +00:00
ca6f6de798 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. 2025-03-17 13:37:48 +00:00
80d2f30804 4.1.15
Some checks failed
Default (tags) / security (push) Failing after 14m48s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-17 13:23:07 +00:00
22f46700f1 fix(connectionhandler): Delay socket termination in TLS session resumption handling to allow proper alert processing 2025-03-17 13:23:07 +00:00
1611f65455 4.1.14
Some checks failed
Default (tags) / security (push) Successful in 21s
Default (tags) / test (push) Failing after 1m9s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-17 13:19:18 +00:00
c6350e271a fix(ConnectionHandler): Use the correct TLS alert data and increase the delay before socket termination when session resumption without SNI is detected. 2025-03-17 13:19:18 +00:00
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
6 changed files with 551 additions and 164 deletions

View File

@ -1,5 +1,116 @@
# Changelog
## 2025-03-18 - 4.2.2 - fix(connectionhandler)
Ensure proper termination of TLS connections without SNI by explicitly ending the socket after sending the unrecognized_name alert. This prevents the connection from hanging and avoids potential duplicate handling.
- Added socket.end() after uncorking the alert packet in ClientHello handling to force connection closure.
- Prevents duplicate data events and ensures the warning alert is processed by clients like Chrome.
## 2025-03-17 - 4.2.1 - fix(core)
No uncommitted changes detected in the project.
## 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.
- Replaced hardcoded alert buffers in ConnectionHandler with calls to the TlsAlert class.
- Removed old warnings and implemented a mechanism to remove existing 'data' listeners and await a new ClientHello.
- Introduced alertFallbackTimeout property in connection records to track fallback timeout and ensure proper cleanup.
- Extended the delay before closing the connection after sending an alert, providing the client more time to retry.
## 2025-03-17 - 4.1.15 - fix(connectionhandler)
Delay socket termination in TLS session resumption handling to allow proper alert processing
- Removed the immediate socket.end() call in finishConnection and moved it inside the setTimeout, ensuring that clients (especially Chrome) have additional time to process the TLS alert before connection termination
- This prevents premature socket closure on ClientHello without SNI when session tickets are disallowed
## 2025-03-17 - 4.1.14 - fix(ConnectionHandler)
Use the correct TLS alert data and increase the delay before socket termination when session resumption without SNI is detected.
- Replaced certificateExpiredAlert with serverNameUnknownAlertData for sending the appropriate alert.
- Increased the cleanup delay from 1000ms to 5000ms to allow a more graceful termination.
## 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)
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",
"version": "4.1.2",
"version": "4.2.2",
"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",

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '4.1.2',
version: '4.2.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.'
}

View File

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

View File

@ -104,6 +104,7 @@ export interface IConnectionRecord {
lockedDomain?: string; // Used to lock this connection to the initial SNI
connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
alertFallbackTimeout?: NodeJS.Timeout; // Timer for fallback after alert
lastActivity: number; // Last activity timestamp for inactivity detection
pendingData: Buffer[]; // Buffer to hold data during connection setup
pendingDataSize: number; // Track total size of pending data

258
ts/classes.pp.tlsalert.ts Normal file
View File

@ -0,0 +1,258 @@
import * as net from 'net';
/**
* TlsAlert class for managing TLS alert messages
*/
export class TlsAlert {
// TLS Alert Levels
static readonly LEVEL_WARNING = 0x01;
static readonly LEVEL_FATAL = 0x02;
// TLS Alert Description Codes - RFC 8446 (TLS 1.3) / RFC 5246 (TLS 1.2)
static readonly CLOSE_NOTIFY = 0x00;
static readonly UNEXPECTED_MESSAGE = 0x0A;
static readonly BAD_RECORD_MAC = 0x14;
static readonly DECRYPTION_FAILED = 0x15; // TLS 1.0 only
static readonly RECORD_OVERFLOW = 0x16;
static readonly DECOMPRESSION_FAILURE = 0x1E; // TLS 1.2 and below
static readonly HANDSHAKE_FAILURE = 0x28;
static readonly NO_CERTIFICATE = 0x29; // SSLv3 only
static readonly BAD_CERTIFICATE = 0x2A;
static readonly UNSUPPORTED_CERTIFICATE = 0x2B;
static readonly CERTIFICATE_REVOKED = 0x2C;
static readonly CERTIFICATE_EXPIRED = 0x2F;
static readonly CERTIFICATE_UNKNOWN = 0x30;
static readonly ILLEGAL_PARAMETER = 0x2F;
static readonly UNKNOWN_CA = 0x30;
static readonly ACCESS_DENIED = 0x31;
static readonly DECODE_ERROR = 0x32;
static readonly DECRYPT_ERROR = 0x33;
static readonly EXPORT_RESTRICTION = 0x3C; // TLS 1.0 only
static readonly PROTOCOL_VERSION = 0x46;
static readonly INSUFFICIENT_SECURITY = 0x47;
static readonly INTERNAL_ERROR = 0x50;
static readonly INAPPROPRIATE_FALLBACK = 0x56;
static readonly USER_CANCELED = 0x5A;
static readonly NO_RENEGOTIATION = 0x64; // TLS 1.2 and below
static readonly MISSING_EXTENSION = 0x6D; // TLS 1.3
static readonly UNSUPPORTED_EXTENSION = 0x6E; // TLS 1.3
static readonly CERTIFICATE_REQUIRED = 0x6F; // TLS 1.3
static readonly UNRECOGNIZED_NAME = 0x70;
static readonly BAD_CERTIFICATE_STATUS_RESPONSE = 0x71;
static readonly BAD_CERTIFICATE_HASH_VALUE = 0x72; // TLS 1.2 and below
static readonly UNKNOWN_PSK_IDENTITY = 0x73;
static readonly CERTIFICATE_REQUIRED_1_3 = 0x74; // TLS 1.3
static readonly NO_APPLICATION_PROTOCOL = 0x78;
/**
* Create a TLS alert buffer with the specified level and description code
*
* @param level Alert level (warning or fatal)
* @param description Alert description code
* @param tlsVersion TLS version bytes (default is TLS 1.2: 0x0303)
* @returns Buffer containing the TLS alert message
*/
static create(
level: number,
description: number,
tlsVersion: [number, number] = [0x03, 0x03]
): Buffer {
return Buffer.from([
0x15, // Alert record type
tlsVersion[0],
tlsVersion[1], // TLS version (default to TLS 1.2: 0x0303)
0x00,
0x02, // Length
level, // Alert level
description, // Alert description
]);
}
/**
* Create a warning-level TLS alert
*
* @param description Alert description code
* @returns Buffer containing the warning-level TLS alert message
*/
static createWarning(description: number): Buffer {
return this.create(this.LEVEL_WARNING, description);
}
/**
* Create a fatal-level TLS alert
*
* @param description Alert description code
* @returns Buffer containing the fatal-level TLS alert message
*/
static createFatal(description: number): Buffer {
return this.create(this.LEVEL_FATAL, description);
}
/**
* Send a TLS alert to a socket and optionally close the connection
*
* @param socket The socket to send the alert to
* @param level Alert level (warning or fatal)
* @param description Alert description code
* @param closeAfterSend Whether to close the connection after sending the alert
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
* @returns Promise that resolves when the alert has been sent
*/
static async send(
socket: net.Socket,
level: number,
description: number,
closeAfterSend: boolean = false,
closeDelay: number = 200
): Promise<void> {
const alert = this.create(level, description);
return new Promise<void>((resolve, reject) => {
try {
// Ensure the alert is written as a single packet
socket.cork();
const writeSuccessful = socket.write(alert, (err) => {
if (err) {
reject(err);
return;
}
if (closeAfterSend) {
setTimeout(() => {
socket.end();
resolve();
}, closeDelay);
} else {
resolve();
}
});
socket.uncork();
// If write wasn't successful immediately, wait for drain
if (!writeSuccessful && !closeAfterSend) {
socket.once('drain', () => {
resolve();
});
}
} catch (err) {
reject(err);
}
});
}
/**
* Pre-defined TLS alert messages
*/
static readonly alerts = {
// Warning level alerts
closeNotify: TlsAlert.createWarning(TlsAlert.CLOSE_NOTIFY),
unsupportedExtension: TlsAlert.createWarning(TlsAlert.UNSUPPORTED_EXTENSION),
certificateRequired: TlsAlert.createWarning(TlsAlert.CERTIFICATE_REQUIRED),
unrecognizedName: TlsAlert.createWarning(TlsAlert.UNRECOGNIZED_NAME),
noRenegotiation: TlsAlert.createWarning(TlsAlert.NO_RENEGOTIATION),
userCanceled: TlsAlert.createWarning(TlsAlert.USER_CANCELED),
// Warning level alerts for session resumption
certificateExpiredWarning: TlsAlert.createWarning(TlsAlert.CERTIFICATE_EXPIRED),
handshakeFailureWarning: TlsAlert.createWarning(TlsAlert.HANDSHAKE_FAILURE),
insufficientSecurityWarning: TlsAlert.createWarning(TlsAlert.INSUFFICIENT_SECURITY),
// Fatal level alerts
unexpectedMessage: TlsAlert.createFatal(TlsAlert.UNEXPECTED_MESSAGE),
badRecordMac: TlsAlert.createFatal(TlsAlert.BAD_RECORD_MAC),
recordOverflow: TlsAlert.createFatal(TlsAlert.RECORD_OVERFLOW),
handshakeFailure: TlsAlert.createFatal(TlsAlert.HANDSHAKE_FAILURE),
badCertificate: TlsAlert.createFatal(TlsAlert.BAD_CERTIFICATE),
certificateExpired: TlsAlert.createFatal(TlsAlert.CERTIFICATE_EXPIRED),
certificateUnknown: TlsAlert.createFatal(TlsAlert.CERTIFICATE_UNKNOWN),
illegalParameter: TlsAlert.createFatal(TlsAlert.ILLEGAL_PARAMETER),
unknownCA: TlsAlert.createFatal(TlsAlert.UNKNOWN_CA),
accessDenied: TlsAlert.createFatal(TlsAlert.ACCESS_DENIED),
decodeError: TlsAlert.createFatal(TlsAlert.DECODE_ERROR),
decryptError: TlsAlert.createFatal(TlsAlert.DECRYPT_ERROR),
protocolVersion: TlsAlert.createFatal(TlsAlert.PROTOCOL_VERSION),
insufficientSecurity: TlsAlert.createFatal(TlsAlert.INSUFFICIENT_SECURITY),
internalError: TlsAlert.createFatal(TlsAlert.INTERNAL_ERROR),
unrecognizedNameFatal: TlsAlert.createFatal(TlsAlert.UNRECOGNIZED_NAME),
};
/**
* Utility method to send a warning-level unrecognized_name alert
* Specifically designed for SNI issues to encourage the client to retry with SNI
*
* @param socket The socket to send the alert to
* @returns Promise that resolves when the alert has been sent
*/
static async sendSniRequired(socket: net.Socket): Promise<void> {
return this.send(socket, this.LEVEL_WARNING, this.UNRECOGNIZED_NAME);
}
/**
* Utility method to send a close_notify alert and close the connection
*
* @param socket The socket to send the alert to
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
* @returns Promise that resolves when the alert has been sent and the connection closed
*/
static async sendCloseNotify(socket: net.Socket, closeDelay: number = 200): Promise<void> {
return this.send(socket, this.LEVEL_WARNING, this.CLOSE_NOTIFY, true, closeDelay);
}
/**
* Utility method to send a certificate_expired alert to force new TLS session
*
* @param socket The socket to send the alert to
* @param fatal Whether to send as a fatal alert (default: false)
* @param closeAfterSend Whether to close the connection after sending the alert (default: true)
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
* @returns Promise that resolves when the alert has been sent
*/
static async sendCertificateExpired(
socket: net.Socket,
fatal: boolean = false,
closeAfterSend: boolean = true,
closeDelay: number = 200
): Promise<void> {
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);
}
}