Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
0fb5e5ea50 | |||
35f6739b3c | |||
4634c68ea6 | |||
e126032b61 | |||
7797c799dd | |||
e8639e1b01 | |||
60a0ad106d | |||
a70c123007 | |||
46aa7620b0 | |||
f72db86e37 | |||
d612df107e | |||
1c34578c36 | |||
1f9943b5a7 | |||
67ddf97547 | |||
8a96b45ece | |||
2b6464acd5 | |||
efbb4335d7 | |||
9dd402054d | |||
6c1efc1dc0 | |||
cad0e6a2b2 | |||
794e1292e5 | |||
ee79f9ab7c |
74
changelog.md
74
changelog.md
@ -1,5 +1,79 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-03-17 - 4.1.13 - fix(tls-handshake)
|
||||||
|
Set certificate_expired TLS alert level to warning instead of fatal to allow graceful termination.
|
||||||
|
|
||||||
|
- In the TLS handshake alert for certificate_expired (0x2F), changed the alert level from 0x02 (fatal) to 0x01 (warning).
|
||||||
|
- This change avoids abrupt connection termination, enabling a smoother handling of certificate expiration alerts.
|
||||||
|
|
||||||
|
## 2025-03-17 - 4.1.12 - fix(classes.pp.connectionhandler)
|
||||||
|
Replace unrecognized_name alert data with certificate_expired alert in TLS handshake handling for session resumption without SNI
|
||||||
|
|
||||||
|
- Switched the alert payload from serverNameUnknownAlertData to a new certificateExpiredAlert buffer
|
||||||
|
- Now sends a fatal certificate_expired alert (code 47) instead of a warning unrecognized_name alert
|
||||||
|
- Improves TLS error reporting and encourages immediate disconnection when a ClientHello lacks SNI and session tickets are disallowed
|
||||||
|
|
||||||
|
## 2025-03-17 - 4.1.11 - fix(connectionhandler)
|
||||||
|
Increase delay before cleaning up connections when session resumption is blocked due to missing SNI, allowing more natural socket termination.
|
||||||
|
|
||||||
|
- Changed cleanup delay in ts/classes.pp.connectionhandler.ts from 300ms to 1000ms.
|
||||||
|
- This fix ensures that sockets get sufficient time to terminate gracefully.
|
||||||
|
|
||||||
|
## 2025-03-16 - 4.1.10 - fix(connectionhandler)
|
||||||
|
Increase delay timings for TLS alert transmission in session ticket blocking to allow graceful socket termination
|
||||||
|
|
||||||
|
- Updated finishConnection: replaced immediate socket.destroy with a graceful end call
|
||||||
|
- Increased delay after successful write from 50ms to 200ms to allow alert processing
|
||||||
|
- Raised safety timeout from 250ms to 400ms when waiting for 'drain' event
|
||||||
|
|
||||||
|
## 2025-03-16 - 4.1.9 - fix(ConnectionHandler)
|
||||||
|
Replace closeNotify alert with handshake failure alert in TLS ClientHello handling to properly signal missing SNI and enforce session ticket restrictions.
|
||||||
|
|
||||||
|
- Switched alert data sent on missing SNI from closeNotifyAlert to sslHandshakeFailureAlertData.
|
||||||
|
- Ensures consistent TLS alert behavior during handshake failure.
|
||||||
|
|
||||||
|
## 2025-03-16 - 4.1.8 - fix(ConnectionHandler/tls)
|
||||||
|
Change the TLS alert sent when a ClientHello lacks SNI: use the close_notify alert instead of handshake_failure to prompt immediate retry with SNI.
|
||||||
|
|
||||||
|
- Replaced the previously sent handshake_failure alert (code 0x28) with a close_notify alert (code 0x00) in the TLS session resumption handling in ConnectionHandler.
|
||||||
|
- This change encourages clients to immediately retry and include SNI when allowSessionTicket is false.
|
||||||
|
|
||||||
|
## 2025-03-16 - 4.1.7 - fix(classes.pp.connectionhandler)
|
||||||
|
Improve TLS alert handling in ClientHello when SNI is missing and session tickets are disallowed
|
||||||
|
|
||||||
|
- Replace the unrecognized_name alert with a handshake_failure alert to ensure better client behavior.
|
||||||
|
- Refactor the alert sending mechanism using cork/uncork and add a safety timeout for the drain event.
|
||||||
|
- Enhance logging for debugging TLS handshake failures when SNI is absent.
|
||||||
|
|
||||||
|
## 2025-03-16 - 4.1.6 - fix(tls)
|
||||||
|
Refine TLS ClientHello handling when allowSessionTicket is false by replacing extensive alert timeout logic with a concise warning alert and short delay, encouraging immediate client retry with proper SNI
|
||||||
|
|
||||||
|
- Update the TLS alert sending mechanism to use cork/uncork and a short, fixed delay instead of long timeouts
|
||||||
|
- Remove redundant event listeners and excessive cleanup logic after sending the alert
|
||||||
|
- Improve code clarity and encourage clients (e.g., Chrome) to retry handshake with SNI more responsively
|
||||||
|
|
||||||
|
## 2025-03-16 - 4.1.5 - fix(TLS/ConnectionHandler)
|
||||||
|
Improve handling of TLS session resumption without SNI by sending an 'unrecognized_name' alert instead of immediately terminating the connection. This change adds a grace period for the client to retry the handshake with proper SNI and cleans up the connection if no valid response is received.
|
||||||
|
|
||||||
|
- Send a TLS warning (unrecognized_name alert, code 112) when a ClientHello is received without SNI and session tickets are disallowed.
|
||||||
|
- Utilize socket cork/uncork to ensure the alert is sent as a single packet.
|
||||||
|
- Add a 5-second alert timeout and a subsequent 30-second grace period to allow clients to initiate a new handshake with SNI.
|
||||||
|
- Clean up and terminate the connection if no valid SNI is provided after the grace period, logging appropriate termination reasons.
|
||||||
|
|
||||||
|
## 2025-03-15 - 4.1.4 - fix(ConnectionHandler)
|
||||||
|
Refactor ConnectionHandler code formatting for improved readability and consistency in log messages and whitespace handling
|
||||||
|
|
||||||
|
- Standardized indentation and spacing in method signatures and log statements
|
||||||
|
- Aligned inline comments and string concatenations for clarity
|
||||||
|
- Minor refactoring of parameter formatting without changing functionality
|
||||||
|
|
||||||
|
## 2025-03-15 - 4.1.3 - fix(connectionhandler)
|
||||||
|
Improve handling of TLS ClientHello messages when allowSessionTicket is disabled and no SNI is provided by sending a warning alert (unrecognized_name, code 0x70) with a proper callback and delay to ensure the alert is transmitted before closing the connection.
|
||||||
|
|
||||||
|
- Replace the fatal alert (0x02/0x40) with a warning alert (0x01/0x70) to notify clients to send SNI.
|
||||||
|
- Use socket.write callback to wait 100ms after sending the alert before terminating the connection.
|
||||||
|
- Remove the previous short (50ms) delay in favor of a more reliable delay mechanism before cleanup.
|
||||||
|
|
||||||
## 2025-03-15 - 4.1.2 - fix(connectionhandler)
|
## 2025-03-15 - 4.1.2 - fix(connectionhandler)
|
||||||
Send proper TLS alert before terminating connections when SNI is missing and session tickets are disallowed.
|
Send proper TLS alert before terminating connections when SNI is missing and session tickets are disallowed.
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "4.1.2",
|
"version": "4.1.13",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.",
|
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '4.1.2',
|
version: '4.1.13',
|
||||||
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
|
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import type { IConnectionRecord, IDomainConfig, IPortProxySettings } from './classes.pp.interfaces.js';
|
import type {
|
||||||
|
IConnectionRecord,
|
||||||
|
IDomainConfig,
|
||||||
|
IPortProxySettings,
|
||||||
|
} from './classes.pp.interfaces.js';
|
||||||
import { ConnectionManager } from './classes.pp.connectionmanager.js';
|
import { ConnectionManager } from './classes.pp.connectionmanager.js';
|
||||||
import { SecurityManager } from './classes.pp.securitymanager.js';
|
import { SecurityManager } from './classes.pp.securitymanager.js';
|
||||||
import { DomainConfigManager } from './classes.pp.domainconfigmanager.js';
|
import { DomainConfigManager } from './classes.pp.domainconfigmanager.js';
|
||||||
@ -73,8 +77,8 @@ export class ConnectionHandler {
|
|||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
|
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
|
||||||
`Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
|
`Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
|
||||||
`Active connections: ${this.connectionManager.getConnectionCount()}`
|
`Active connections: ${this.connectionManager.getConnectionCount()}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
@ -94,7 +98,10 @@ export class ConnectionHandler {
|
|||||||
/**
|
/**
|
||||||
* Handle a connection that should be forwarded to NetworkProxy
|
* Handle a connection that should be forwarded to NetworkProxy
|
||||||
*/
|
*/
|
||||||
private handleNetworkProxyConnection(socket: plugins.net.Socket, record: IConnectionRecord): void {
|
private handleNetworkProxyConnection(
|
||||||
|
socket: plugins.net.Socket,
|
||||||
|
record: IConnectionRecord
|
||||||
|
): void {
|
||||||
const connectionId = record.id;
|
const connectionId = record.id;
|
||||||
let initialDataReceived = false;
|
let initialDataReceived = false;
|
||||||
|
|
||||||
@ -104,7 +111,7 @@ export class ConnectionHandler {
|
|||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
|
`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add a grace period instead of immediate termination
|
// Add a grace period instead of immediate termination
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!initialDataReceived) {
|
if (!initialDataReceived) {
|
||||||
@ -144,7 +151,7 @@ export class ConnectionHandler {
|
|||||||
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
|
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Non-TLS connection detected on port 443. ` +
|
`[${connectionId}] Non-TLS connection detected on port 443. ` +
|
||||||
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
|
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
|
||||||
);
|
);
|
||||||
if (record.incomingTerminationReason === null) {
|
if (record.incomingTerminationReason === null) {
|
||||||
record.incomingTerminationReason = 'non_tls_blocked';
|
record.incomingTerminationReason = 'non_tls_blocked';
|
||||||
@ -159,8 +166,8 @@ export class ConnectionHandler {
|
|||||||
if (this.tlsManager.isTlsHandshake(chunk)) {
|
if (this.tlsManager.isTlsHandshake(chunk)) {
|
||||||
record.isTLS = true;
|
record.isTLS = true;
|
||||||
|
|
||||||
// Check session tickets if they're disabled
|
// Check for ClientHello to extract SNI - but don't enforce it for NetworkProxy
|
||||||
if (this.settings.allowSessionTicket === false && this.tlsManager.isClientHello(chunk)) {
|
if (this.tlsManager.isClientHello(chunk)) {
|
||||||
// Create connection info for SNI extraction
|
// Create connection info for SNI extraction
|
||||||
const connInfo = {
|
const connInfo = {
|
||||||
sourceIp: record.remoteIP,
|
sourceIp: record.remoteIP,
|
||||||
@ -169,73 +176,46 @@ export class ConnectionHandler {
|
|||||||
destPort: socket.localPort || 0,
|
destPort: socket.localPort || 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract SNI for domain-specific NetworkProxy handling
|
// Extract SNI for domain-specific NetworkProxy handling if available
|
||||||
const serverName = this.tlsManager.extractSNI(chunk, connInfo);
|
const serverName = this.tlsManager.extractSNI(chunk, connInfo);
|
||||||
|
|
||||||
// If allowSessionTicket is false and we can't determine SNI, terminate the connection
|
// For NetworkProxy connections, we'll allow session tickets even without SNI
|
||||||
if (!serverName) {
|
// We'll only use the serverName if available to determine the specific NetworkProxy port
|
||||||
// Always block when allowSessionTicket is false and there's no SNI
|
if (serverName) {
|
||||||
// Don't even check for session resumption - be strict
|
// Save domain config and SNI in connection record
|
||||||
console.log(
|
const domainConfig = this.domainConfigManager.findDomainConfig(serverName);
|
||||||
`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` +
|
record.domainConfig = domainConfig;
|
||||||
`Terminating connection to force new TLS handshake with SNI.`
|
record.lockedDomain = serverName;
|
||||||
);
|
|
||||||
|
|
||||||
// Send a proper TLS alert before ending the connection
|
|
||||||
// This helps browsers like Chrome properly recognize the error
|
|
||||||
const alertData = Buffer.from([
|
|
||||||
0x15, // Alert record type
|
|
||||||
0x03, 0x03, // TLS 1.2 version
|
|
||||||
0x00, 0x02, // Length
|
|
||||||
0x02, // Fatal alert level
|
|
||||||
0x40 // Handshake failure alert
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
socket.write(alertData);
|
|
||||||
} catch (err) {
|
|
||||||
// Ignore write errors, we're closing anyway
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record.incomingTerminationReason === null) {
|
|
||||||
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
|
||||||
this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a small delay before ending to allow alert to be sent
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.end();
|
|
||||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save domain config and SNI in connection record
|
// Use domain-specific NetworkProxy port if configured
|
||||||
const domainConfig = this.domainConfigManager.findDomainConfig(serverName);
|
if (domainConfig && this.domainConfigManager.shouldUseNetworkProxy(domainConfig)) {
|
||||||
record.domainConfig = domainConfig;
|
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
|
||||||
record.lockedDomain = serverName;
|
|
||||||
|
|
||||||
// Use domain-specific NetworkProxy port if configured
|
if (this.settings.enableDetailedLogging) {
|
||||||
if (domainConfig && this.domainConfigManager.shouldUseNetworkProxy(domainConfig)) {
|
console.log(
|
||||||
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
|
`[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
// Forward to NetworkProxy with domain-specific port
|
||||||
console.log(
|
this.networkProxyBridge.forwardToNetworkProxy(
|
||||||
`[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`
|
connectionId,
|
||||||
|
socket,
|
||||||
|
record,
|
||||||
|
chunk,
|
||||||
|
networkProxyPort,
|
||||||
|
(reason) => this.connectionManager.initiateCleanupOnce(record, reason)
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
// Forward to NetworkProxy with domain-specific port
|
this.settings.allowSessionTicket === false &&
|
||||||
this.networkProxyBridge.forwardToNetworkProxy(
|
this.settings.enableDetailedLogging
|
||||||
connectionId,
|
) {
|
||||||
socket,
|
// Log that we're allowing a session resumption without SNI for NetworkProxy
|
||||||
record,
|
console.log(
|
||||||
chunk,
|
`[${connectionId}] Allowing session resumption without SNI for NetworkProxy forwarding`
|
||||||
networkProxyPort,
|
|
||||||
(reason) => this.connectionManager.initiateCleanupOnce(record, reason)
|
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,14 +230,10 @@ export class ConnectionHandler {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// If not TLS, use normal direct connection
|
// If not TLS, use normal direct connection
|
||||||
console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${record.localPort}`);
|
console.log(
|
||||||
this.setupDirectConnection(
|
`[${connectionId}] Non-TLS connection on NetworkProxy port ${record.localPort}`
|
||||||
socket,
|
|
||||||
record,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
chunk
|
|
||||||
);
|
);
|
||||||
|
this.setupDirectConnection(socket, record, undefined, undefined, chunk);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -290,7 +266,7 @@ export class ConnectionHandler {
|
|||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
|
`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add a grace period instead of immediate termination
|
// Add a grace period instead of immediate termination
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!initialDataReceived) {
|
if (!initialDataReceived) {
|
||||||
@ -375,14 +351,13 @@ export class ConnectionHandler {
|
|||||||
record.domainConfig = domainConfig;
|
record.domainConfig = domainConfig;
|
||||||
|
|
||||||
// Check if this domain should use NetworkProxy (domain-specific setting)
|
// Check if this domain should use NetworkProxy (domain-specific setting)
|
||||||
if (domainConfig &&
|
if (
|
||||||
this.domainConfigManager.shouldUseNetworkProxy(domainConfig) &&
|
domainConfig &&
|
||||||
this.networkProxyBridge.getNetworkProxy()) {
|
this.domainConfigManager.shouldUseNetworkProxy(domainConfig) &&
|
||||||
|
this.networkProxyBridge.getNetworkProxy()
|
||||||
|
) {
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(
|
console.log(`[${connectionId}] Domain ${serverName} is configured to use NetworkProxy`);
|
||||||
`[${connectionId}] Domain ${serverName} is configured to use NetworkProxy`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
|
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
|
||||||
@ -404,23 +379,24 @@ export class ConnectionHandler {
|
|||||||
// IP validation
|
// IP validation
|
||||||
if (domainConfig) {
|
if (domainConfig) {
|
||||||
const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig);
|
const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig);
|
||||||
|
|
||||||
// Skip IP validation if allowedIPs is empty
|
// Skip IP validation if allowedIPs is empty
|
||||||
if (
|
if (
|
||||||
domainConfig.allowedIPs.length > 0 &&
|
domainConfig.allowedIPs.length > 0 &&
|
||||||
!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)
|
!this.securityManager.isIPAuthorized(
|
||||||
|
record.remoteIP,
|
||||||
|
ipRules.allowedIPs,
|
||||||
|
ipRules.blockedIPs
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return rejectIncomingConnection(
|
return rejectIncomingConnection(
|
||||||
'rejected',
|
'rejected',
|
||||||
`Connection rejected: IP ${record.remoteIP} not allowed for domain ${domainConfig.domains.join(
|
`Connection rejected: IP ${
|
||||||
', '
|
record.remoteIP
|
||||||
)}`
|
} not allowed for domain ${domainConfig.domains.join(', ')}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
||||||
this.settings.defaultAllowedIPs &&
|
|
||||||
this.settings.defaultAllowedIPs.length > 0
|
|
||||||
) {
|
|
||||||
if (
|
if (
|
||||||
!this.securityManager.isIPAuthorized(
|
!this.securityManager.isIPAuthorized(
|
||||||
record.remoteIP,
|
record.remoteIP,
|
||||||
@ -487,28 +463,36 @@ export class ConnectionHandler {
|
|||||||
} else {
|
} else {
|
||||||
// Attempt to find a matching forced domain config based on the local port.
|
// Attempt to find a matching forced domain config based on the local port.
|
||||||
const forcedDomain = this.domainConfigManager.findDomainConfigForPort(localPort);
|
const forcedDomain = this.domainConfigManager.findDomainConfigForPort(localPort);
|
||||||
|
|
||||||
if (forcedDomain) {
|
if (forcedDomain) {
|
||||||
const ipRules = this.domainConfigManager.getEffectiveIPRules(forcedDomain);
|
const ipRules = this.domainConfigManager.getEffectiveIPRules(forcedDomain);
|
||||||
|
|
||||||
if (!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)) {
|
if (
|
||||||
|
!this.securityManager.isIPAuthorized(
|
||||||
|
record.remoteIP,
|
||||||
|
ipRules.allowedIPs,
|
||||||
|
ipRules.blockedIPs
|
||||||
|
)
|
||||||
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Connection from ${record.remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(
|
`[${connectionId}] Connection from ${
|
||||||
|
record.remoteIP
|
||||||
|
} rejected: IP not allowed for domain ${forcedDomain.domains.join(
|
||||||
', '
|
', '
|
||||||
)} on port ${localPort}.`
|
)} on port ${localPort}.`
|
||||||
);
|
);
|
||||||
socket.end();
|
socket.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Port-based connection from ${record.remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(
|
`[${connectionId}] Port-based connection from ${
|
||||||
', '
|
record.remoteIP
|
||||||
)}.`
|
} on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupConnection('', undefined, forcedDomain, localPort);
|
setupConnection('', undefined, forcedDomain, localPort);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -526,14 +510,14 @@ export class ConnectionHandler {
|
|||||||
clearTimeout(initialTimeout);
|
clearTimeout(initialTimeout);
|
||||||
initialTimeout = null;
|
initialTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
initialDataReceived = true;
|
initialDataReceived = true;
|
||||||
|
|
||||||
// Block non-TLS connections on port 443
|
// Block non-TLS connections on port 443
|
||||||
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
|
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Non-TLS connection detected on port 443 in SNI handler. ` +
|
`[${connectionId}] Non-TLS connection detected on port 443 in SNI handler. ` +
|
||||||
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
|
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
|
||||||
);
|
);
|
||||||
if (record.incomingTerminationReason === null) {
|
if (record.incomingTerminationReason === null) {
|
||||||
record.incomingTerminationReason = 'non_tls_blocked';
|
record.incomingTerminationReason = 'non_tls_blocked';
|
||||||
@ -566,45 +550,112 @@ export class ConnectionHandler {
|
|||||||
|
|
||||||
// Extract SNI
|
// Extract SNI
|
||||||
serverName = this.tlsManager.extractSNI(chunk, connInfo) || '';
|
serverName = this.tlsManager.extractSNI(chunk, connInfo) || '';
|
||||||
|
|
||||||
// If allowSessionTicket is false and this is a ClientHello with no SNI, terminate the connection
|
// If allowSessionTicket is false and this is a ClientHello with no SNI, terminate the connection
|
||||||
if (this.settings.allowSessionTicket === false &&
|
if (
|
||||||
this.tlsManager.isClientHello(chunk) &&
|
this.settings.allowSessionTicket === false &&
|
||||||
!serverName) {
|
this.tlsManager.isClientHello(chunk) &&
|
||||||
|
!serverName
|
||||||
// Always block ClientHello without SNI when allowSessionTicket is false
|
) {
|
||||||
// Don't even check for session resumption - be strict
|
// Block ClientHello without SNI when allowSessionTicket is false
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` +
|
`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` +
|
||||||
`Terminating connection to force new TLS handshake with SNI.`
|
`Sending warning unrecognized_name alert to encourage immediate retry with SNI.`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send a proper TLS alert before ending the connection
|
// Set the termination reason first
|
||||||
const alertData = Buffer.from([
|
|
||||||
0x15, // Alert record type
|
|
||||||
0x03, 0x03, // TLS 1.2 version
|
|
||||||
0x00, 0x02, // Length
|
|
||||||
0x02, // Fatal alert level
|
|
||||||
0x40 // Handshake failure alert
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
socket.write(alertData);
|
|
||||||
} catch (err) {
|
|
||||||
// Ignore write errors, we're closing anyway
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record.incomingTerminationReason === null) {
|
if (record.incomingTerminationReason === null) {
|
||||||
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
||||||
this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni');
|
this.connectionManager.incrementTerminationStat(
|
||||||
|
'incoming',
|
||||||
|
'session_ticket_blocked_no_sni'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a small delay before ending to allow alert to be sent
|
// Create a warning-level alert for unrecognized_name
|
||||||
setTimeout(() => {
|
// This encourages Chrome to retry immediately with SNI
|
||||||
|
const serverNameUnknownAlertData = Buffer.from([
|
||||||
|
0x15, // Alert record type
|
||||||
|
0x03,
|
||||||
|
0x03, // TLS 1.2 version
|
||||||
|
0x00,
|
||||||
|
0x02, // Length
|
||||||
|
0x01, // Warning alert level (not fatal)
|
||||||
|
0x70, // unrecognized_name alert (code 112)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Send a handshake_failure alert instead of unrecognized_name
|
||||||
|
const sslHandshakeFailureAlertData = Buffer.from([
|
||||||
|
0x15, // Alert record type
|
||||||
|
0x03,
|
||||||
|
0x03, // TLS 1.2 version
|
||||||
|
0x00,
|
||||||
|
0x02, // Length
|
||||||
|
0x01, // Warning alert level (not fatal)
|
||||||
|
0x28, // handshake_failure alert (40) instead of unrecognized_name (112)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const closeNotifyAlert = Buffer.from([
|
||||||
|
0x15, // Alert record type
|
||||||
|
0x03,
|
||||||
|
0x03, // TLS 1.2 version
|
||||||
|
0x00,
|
||||||
|
0x02, // Length
|
||||||
|
0x01, // Warning alert level (1)
|
||||||
|
0x00, // close_notify alert (0)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const certificateExpiredAlert = Buffer.from([
|
||||||
|
0x15, // Alert record type
|
||||||
|
0x03,
|
||||||
|
0x03, // TLS 1.2 version
|
||||||
|
0x00,
|
||||||
|
0x02, // Length
|
||||||
|
0x01, // Warning alert level (1)
|
||||||
|
0x2F, // certificate_expired alert (47)
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use cork/uncork to ensure the alert is sent as a single packet
|
||||||
|
socket.cork();
|
||||||
|
const writeSuccessful = socket.write(certificateExpiredAlert);
|
||||||
|
socket.uncork();
|
||||||
|
|
||||||
|
// Function to handle the clean socket termination - but more gradually
|
||||||
|
const finishConnection = () => {
|
||||||
|
// Give Chrome more time to process the alert before closing
|
||||||
|
// We won't call destroy() at all - just end() and let the socket close naturally
|
||||||
|
socket.end();
|
||||||
|
|
||||||
|
// Log the cleanup but wait for natural closure
|
||||||
|
setTimeout(() => {
|
||||||
|
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||||
|
}, 1000); // Longer delay to let socket cleanup happen naturally
|
||||||
|
};
|
||||||
|
|
||||||
|
if (writeSuccessful) {
|
||||||
|
// Wait longer before ending connection to ensure alert is processed by client
|
||||||
|
setTimeout(finishConnection, 200); // Increased from 50ms to 200ms
|
||||||
|
} else {
|
||||||
|
// If the kernel buffer was full, wait for the drain event
|
||||||
|
socket.once('drain', () => {
|
||||||
|
// Wait longer after drain as well
|
||||||
|
setTimeout(finishConnection, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Safety timeout is increased too
|
||||||
|
setTimeout(() => {
|
||||||
|
socket.removeAllListeners('drain');
|
||||||
|
finishConnection();
|
||||||
|
}, 400); // Increased from 250ms to 400ms
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// If we can't send the alert, fall back to immediate termination
|
||||||
|
console.log(`[${connectionId}] Error sending TLS alert: ${err.message}`);
|
||||||
socket.end();
|
socket.end();
|
||||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||||
}, 50);
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -653,23 +704,21 @@ export class ConnectionHandler {
|
|||||||
overridePort?: number
|
overridePort?: number
|
||||||
): void {
|
): void {
|
||||||
const connectionId = record.id;
|
const connectionId = record.id;
|
||||||
|
|
||||||
// Determine target host
|
// Determine target host
|
||||||
const targetHost = domainConfig
|
const targetHost = domainConfig
|
||||||
? this.domainConfigManager.getTargetIP(domainConfig)
|
? this.domainConfigManager.getTargetIP(domainConfig)
|
||||||
: this.settings.targetIP!;
|
: this.settings.targetIP!;
|
||||||
|
|
||||||
// Determine target port
|
// Determine target port
|
||||||
const targetPort = overridePort !== undefined
|
const targetPort = overridePort !== undefined ? overridePort : this.settings.toPort;
|
||||||
? overridePort
|
|
||||||
: this.settings.toPort;
|
|
||||||
|
|
||||||
// Setup connection options
|
// Setup connection options
|
||||||
const connectionOptions: plugins.net.NetConnectOpts = {
|
const connectionOptions: plugins.net.NetConnectOpts = {
|
||||||
host: targetHost,
|
host: targetHost,
|
||||||
port: targetPort,
|
port: targetPort,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Preserve source IP if configured
|
// Preserve source IP if configured
|
||||||
if (this.settings.preserveSourceIP) {
|
if (this.settings.preserveSourceIP) {
|
||||||
connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
|
connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
|
||||||
@ -926,18 +975,20 @@ export class ConnectionHandler {
|
|||||||
|
|
||||||
// Process any remaining data in the queue before switching to piping
|
// Process any remaining data in the queue before switching to piping
|
||||||
processDataQueue();
|
processDataQueue();
|
||||||
|
|
||||||
// Set up piping immediately
|
// Set up piping immediately
|
||||||
pipingEstablished = true;
|
pipingEstablished = true;
|
||||||
|
|
||||||
// Flush all pending data to target
|
// Flush all pending data to target
|
||||||
if (record.pendingData.length > 0) {
|
if (record.pendingData.length > 0) {
|
||||||
const combinedData = Buffer.concat(record.pendingData);
|
const combinedData = Buffer.concat(record.pendingData);
|
||||||
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`);
|
console.log(
|
||||||
|
`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write pending data immediately
|
// Write pending data immediately
|
||||||
targetSocket.write(combinedData, (err) => {
|
targetSocket.write(combinedData, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -945,19 +996,19 @@ export class ConnectionHandler {
|
|||||||
return this.connectionManager.initiateCleanupOnce(record, 'write_error');
|
return this.connectionManager.initiateCleanupOnce(record, 'write_error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear the buffer now that we've processed it
|
// Clear the buffer now that we've processed it
|
||||||
record.pendingData = [];
|
record.pendingData = [];
|
||||||
record.pendingDataSize = 0;
|
record.pendingDataSize = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup piping in both directions without any delays
|
// Setup piping in both directions without any delays
|
||||||
socket.pipe(targetSocket);
|
socket.pipe(targetSocket);
|
||||||
targetSocket.pipe(socket);
|
targetSocket.pipe(socket);
|
||||||
|
|
||||||
// Resume the socket to ensure data flows
|
// Resume the socket to ensure data flows
|
||||||
socket.resume();
|
socket.resume();
|
||||||
|
|
||||||
// Process any data that might be queued in the interim
|
// Process any data that might be queued in the interim
|
||||||
if (dataQueue.length > 0) {
|
if (dataQueue.length > 0) {
|
||||||
// Write any remaining queued data directly to the target socket
|
// Write any remaining queued data directly to the target socket
|
||||||
@ -968,7 +1019,7 @@ export class ConnectionHandler {
|
|||||||
dataQueue.length = 0;
|
dataQueue.length = 0;
|
||||||
queueSize = 0;
|
queueSize = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
`[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
||||||
@ -1033,15 +1084,12 @@ export class ConnectionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set connection timeout
|
// Set connection timeout
|
||||||
record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(
|
record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
|
||||||
record,
|
console.log(
|
||||||
(record, reason) => {
|
`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.`
|
||||||
console.log(
|
);
|
||||||
`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.`
|
this.connectionManager.initiateCleanupOnce(record, reason);
|
||||||
);
|
});
|
||||||
this.connectionManager.initiateCleanupOnce(record, reason);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mark TLS handshake as complete for TLS connections
|
// Mark TLS handshake as complete for TLS connections
|
||||||
if (record.isTLS) {
|
if (record.isTLS) {
|
||||||
@ -1055,4 +1103,4 @@ export class ConnectionHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user