Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
cc04e8786c | |||
9cb6e397b9 | |||
11b65bf684 | |||
4b30e377b9 | |||
b10f35be4b | |||
426249e70e | |||
ba0d9d0b8e | |||
151b8f498c | |||
0db4b07b22 | |||
b55e2da23e | |||
3593e411cf | |||
ca6f6de798 | |||
80d2f30804 | |||
22f46700f1 | |||
1611f65455 | |||
c6350e271a | |||
0fb5e5ea50 | |||
35f6739b3c | |||
4634c68ea6 | |||
e126032b61 | |||
7797c799dd | |||
e8639e1b01 | |||
60a0ad106d | |||
a70c123007 | |||
46aa7620b0 | |||
f72db86e37 | |||
d612df107e | |||
1c34578c36 | |||
1f9943b5a7 | |||
67ddf97547 | |||
8a96b45ece | |||
2b6464acd5 | |||
efbb4335d7 | |||
9dd402054d | |||
6c1efc1dc0 | |||
cad0e6a2b2 | |||
794e1292e5 | |||
ee79f9ab7c | |||
107bc3b50b | |||
97982976c8 | |||
fe60f88746 | |||
252a987344 |
137
changelog.md
137
changelog.md
@ -1,5 +1,142 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-03-18 - 4.2.4 - fix(ts/index.ts)
|
||||
Fix export order in ts/index.ts by moving the port proxy export back and adding interfaces export for proper module exposure
|
||||
|
||||
- Reorder exports to place './classes.pp.portproxy.js' in the correct position
|
||||
- Add export for './classes.pp.interfaces.js' to expose internal interfaces
|
||||
|
||||
## 2025-03-18 - 4.2.3 - fix(connectionhandler)
|
||||
Remove unnecessary delay in TLS session ticket handling for connections without SNI
|
||||
|
||||
- Eliminated the extra setTimeout waiting period before cleaning up connections flagged as session_ticket_blocked_no_sni
|
||||
- Ensures immediate cleanup and improves connection responsiveness during TLS handshake failures
|
||||
|
||||
## 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.
|
||||
|
||||
- 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.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "4.1.0",
|
||||
"version": "4.2.4",
|
||||
"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.1.0',
|
||||
version: '4.2.4',
|
||||
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.'
|
||||
}
|
||||
|
@ -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,14 +176,14 @@ 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);
|
||||
|
||||
// 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) {
|
||||
// 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
|
||||
const domainConfig = this.domainConfigManager.findDomainConfig(serverName);
|
||||
record.domainConfig = domainConfig;
|
||||
record.lockedDomain = serverName;
|
||||
|
||||
@ -201,6 +208,14 @@ export class ConnectionHandler {
|
||||
);
|
||||
return;
|
||||
}
|
||||
} 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`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -255,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) {
|
||||
@ -340,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);
|
||||
@ -369,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,
|
||||
@ -452,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;
|
||||
}
|
||||
@ -491,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';
|
||||
@ -531,6 +550,77 @@ 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
|
||||
) {
|
||||
// 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.`
|
||||
);
|
||||
|
||||
// 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)
|
||||
]);
|
||||
|
||||
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 = () => {
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
};
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Lock the connection to the negotiated SNI.
|
||||
@ -577,23 +667,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:', '');
|
||||
@ -850,18 +938,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) {
|
||||
@ -869,19 +959,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
|
||||
@ -892,7 +982,7 @@ export class ConnectionHandler {
|
||||
dataQueue.length = 0;
|
||||
queueSize = 0;
|
||||
}
|
||||
|
||||
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
||||
@ -957,15 +1047,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) {
|
||||
@ -979,4 +1066,4 @@ export class ConnectionHandler {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -22,135 +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
|
||||
|
||||
// Session tracking for tab reactivation scenarios
|
||||
private static sessionCache: Map<
|
||||
string,
|
||||
{
|
||||
sni: string;
|
||||
timestamp: number;
|
||||
clientRandom?: Buffer;
|
||||
}
|
||||
> = new Map();
|
||||
|
||||
// Longer timeout for session cache (24 hours by default)
|
||||
private static sessionCacheTimeout: number = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
||||
|
||||
// Cleanup interval for session cache (run every hour)
|
||||
private static sessionCleanupInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
/**
|
||||
* Initialize the session cache cleanup mechanism.
|
||||
* This should be called during application startup.
|
||||
*/
|
||||
public static initSessionCacheCleanup(): void {
|
||||
if (this.sessionCleanupInterval === null) {
|
||||
this.sessionCleanupInterval = setInterval(() => {
|
||||
this.cleanupSessionCache();
|
||||
}, 60 * 60 * 1000); // Run every hour
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up expired entries from the session cache
|
||||
*/
|
||||
private static cleanupSessionCache(): void {
|
||||
const now = Date.now();
|
||||
const expiredKeys: string[] = [];
|
||||
|
||||
this.sessionCache.forEach((session, key) => {
|
||||
if (now - session.timestamp > this.sessionCacheTimeout) {
|
||||
expiredKeys.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
expiredKeys.forEach((key) => {
|
||||
this.sessionCache.delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a client identity key for session tracking
|
||||
* Uses source IP and optional client random for uniqueness
|
||||
*
|
||||
* @param sourceIp - Client IP address
|
||||
* @param clientRandom - Optional TLS client random value
|
||||
* @returns A string key for the session cache
|
||||
*/
|
||||
private static createClientKey(sourceIp: string, clientRandom?: Buffer): string {
|
||||
if (clientRandom) {
|
||||
// If we have the client random, use it for more precise tracking
|
||||
return `${sourceIp}:${clientRandom.toString('hex')}`;
|
||||
}
|
||||
// Fall back to just IP-based tracking
|
||||
return sourceIp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store SNI information in the session cache
|
||||
*
|
||||
* @param sourceIp - Client IP address
|
||||
* @param sni - The extracted SNI value
|
||||
* @param clientRandom - Optional TLS client random value
|
||||
*/
|
||||
private static cacheSession(sourceIp: string, sni: string, clientRandom?: Buffer): void {
|
||||
const key = this.createClientKey(sourceIp, clientRandom);
|
||||
this.sessionCache.set(key, {
|
||||
sni,
|
||||
timestamp: Date.now(),
|
||||
clientRandom,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve SNI information from the session cache
|
||||
*
|
||||
* @param sourceIp - Client IP address
|
||||
* @param clientRandom - Optional TLS client random value
|
||||
* @returns The cached SNI or undefined if not found
|
||||
*/
|
||||
private static getCachedSession(sourceIp: string, clientRandom?: Buffer): string | undefined {
|
||||
// Try with client random first for precision
|
||||
if (clientRandom) {
|
||||
const preciseKey = this.createClientKey(sourceIp, clientRandom);
|
||||
const preciseSession = this.sessionCache.get(preciseKey);
|
||||
if (preciseSession) {
|
||||
return preciseSession.sni;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to IP-only lookup
|
||||
const ipKey = this.createClientKey(sourceIp);
|
||||
const session = this.sessionCache.get(ipKey);
|
||||
if (session) {
|
||||
// Update the timestamp to keep the session alive
|
||||
session.timestamp = Date.now();
|
||||
return session.sni;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -1172,7 +1043,6 @@ export class SniHandler {
|
||||
* 4. Fragmented ClientHello messages
|
||||
* 5. TLS 1.3 Early Data (0-RTT)
|
||||
* 6. Chrome's connection racing behaviors
|
||||
* 7. Tab reactivation patterns with session cache
|
||||
*
|
||||
* @param buffer - The buffer containing the TLS ClientHello message
|
||||
* @param connectionInfo - Optional connection information for fragment handling
|
||||
@ -1235,19 +1105,10 @@ export class SniHandler {
|
||||
const standardSni = this.extractSNI(processBuffer, enableLogging);
|
||||
if (standardSni) {
|
||||
log(`Found standard SNI: ${standardSni}`);
|
||||
|
||||
// If we extracted a standard SNI, cache it for future use
|
||||
if (connectionInfo?.sourceIp) {
|
||||
const clientRandom = this.extractClientRandom(processBuffer);
|
||||
this.cacheSession(connectionInfo.sourceIp, standardSni, clientRandom);
|
||||
log(`Cached SNI for future reference: ${standardSni}`);
|
||||
}
|
||||
|
||||
return standardSni;
|
||||
}
|
||||
|
||||
// Check for session resumption when standard SNI extraction fails
|
||||
// This may help in chained proxy scenarios
|
||||
if (this.isClientHello(processBuffer)) {
|
||||
const resumptionInfo = this.hasSessionResumption(processBuffer, enableLogging);
|
||||
|
||||
@ -1258,31 +1119,11 @@ export class SniHandler {
|
||||
const pskSni = this.extractSNIFromPSKExtension(processBuffer, enableLogging);
|
||||
if (pskSni) {
|
||||
log(`Extracted SNI from PSK extension: ${pskSni}`);
|
||||
|
||||
// Cache this SNI
|
||||
if (connectionInfo?.sourceIp) {
|
||||
const clientRandom = this.extractClientRandom(processBuffer);
|
||||
this.cacheSession(connectionInfo.sourceIp, pskSni, clientRandom);
|
||||
}
|
||||
|
||||
return pskSni;
|
||||
}
|
||||
|
||||
// If session resumption has SNI in a non-standard location,
|
||||
// we need to apply heuristics
|
||||
if (connectionInfo?.sourceIp) {
|
||||
const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
||||
if (cachedSni) {
|
||||
log(`Using cached SNI for session resumption: ${cachedSni}`);
|
||||
return cachedSni;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try tab reactivation and other recovery methods...
|
||||
// (existing code remains unchanged)
|
||||
|
||||
// Log detailed info about the ClientHello when SNI extraction fails
|
||||
if (this.isClientHello(processBuffer) && enableLogging) {
|
||||
log(`SNI extraction failed for ClientHello. Buffer details:`);
|
||||
@ -1303,7 +1144,6 @@ export class SniHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Existing code for fallback methods continues...
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -1321,7 +1161,6 @@ export class SniHandler {
|
||||
* @param cachedSni - Optional cached SNI from previous connections (for racing detection)
|
||||
* @returns The extracted server name or undefined if not found or more data needed
|
||||
*/
|
||||
|
||||
public static processTlsPacket(
|
||||
buffer: Buffer,
|
||||
connectionInfo: {
|
||||
@ -1357,19 +1196,12 @@ 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;
|
||||
}
|
||||
|
||||
// Otherwise check our session cache
|
||||
const sessionCachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
||||
if (sessionCachedSni) {
|
||||
log(`Using session-cached SNI for application data: ${sessionCachedSni}`);
|
||||
return sessionCachedSni;
|
||||
}
|
||||
|
||||
log('Application data packet without cached SNI, cannot determine hostname');
|
||||
return undefined;
|
||||
}
|
||||
@ -1385,9 +1217,6 @@ export class SniHandler {
|
||||
const standardSni = this.extractSNI(buffer, enableLogging);
|
||||
if (standardSni) {
|
||||
log(`Found standard SNI in session resumption: ${standardSni}`);
|
||||
|
||||
// Cache this SNI
|
||||
this.cacheSession(connectionInfo.sourceIp, standardSni);
|
||||
return standardSni;
|
||||
}
|
||||
|
||||
@ -1396,7 +1225,6 @@ export class SniHandler {
|
||||
const pskSni = this.extractSNIFromPSKExtension(buffer, enableLogging);
|
||||
if (pskSni) {
|
||||
log(`Extracted SNI from PSK extension: ${pskSni}`);
|
||||
this.cacheSession(connectionInfo.sourceIp, pskSni);
|
||||
return pskSni;
|
||||
}
|
||||
|
||||
@ -1430,13 +1258,6 @@ export class SniHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have SNI, check for cached sessions
|
||||
const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
||||
if (cachedSni) {
|
||||
log(`Using cached SNI for session resumption: ${cachedSni}`);
|
||||
return cachedSni;
|
||||
}
|
||||
|
||||
log(`Session resumption without extractable SNI`);
|
||||
// If allowSessionTicket=false, should be rejected by caller
|
||||
}
|
||||
@ -1451,16 +1272,7 @@ export class SniHandler {
|
||||
}
|
||||
|
||||
// If we couldn't extract an SNI, check if this is a valid ClientHello
|
||||
// If it is, but we couldn't get an SNI, it might be a fragment or
|
||||
// a connection race situation
|
||||
if (this.isClientHello(buffer)) {
|
||||
// Check if we have a cached session for this IP
|
||||
const sessionCachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
||||
if (sessionCachedSni) {
|
||||
log(`Using session cache for ClientHello without SNI: ${sessionCachedSni}`);
|
||||
return sessionCachedSni;
|
||||
}
|
||||
|
||||
log('Valid ClientHello detected, but no SNI extracted - might need more data');
|
||||
}
|
||||
|
||||
|
258
ts/classes.pp.tlsalert.ts
Normal file
258
ts/classes.pp.tlsalert.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
export * from './classes.iptablesproxy.js';
|
||||
export * from './classes.networkproxy.js';
|
||||
export * from './classes.pp.portproxy.js';
|
||||
export * from './classes.port80handler.js';
|
||||
export * from './classes.sslredirect.js';
|
||||
export * from './classes.pp.portproxy.js';
|
||||
export * from './classes.pp.snihandler.js';
|
||||
export * from './classes.pp.interfaces.js';
|
||||
|
Reference in New Issue
Block a user