Compare commits
	
		
			10 Commits
		
	
	
		
			v4.1.3
			...
			d612df107e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d612df107e | |||
| 1c34578c36 | |||
| 1f9943b5a7 | |||
| 67ddf97547 | |||
| 8a96b45ece | |||
| 2b6464acd5 | |||
| efbb4335d7 | |||
| 9dd402054d | |||
| 6c1efc1dc0 | |||
| cad0e6a2b2 | 
							
								
								
									
										35
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								changelog.md
									
									
									
									
									
								
							@@ -1,5 +1,40 @@
 | 
				
			|||||||
# Changelog
 | 
					# Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 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)
 | 
					## 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.
 | 
					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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "@push.rocks/smartproxy",
 | 
					  "name": "@push.rocks/smartproxy",
 | 
				
			||||||
  "version": "4.1.3",
 | 
					  "version": "4.1.8",
 | 
				
			||||||
  "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.3',
 | 
					  version: '4.1.8',
 | 
				
			||||||
  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';
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -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,58 +176,12 @@ 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) {
 | 
				
			||||||
            console.log(
 | 
					 | 
				
			||||||
              `[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` +
 | 
					 | 
				
			||||||
              `Terminating connection to force new TLS handshake with SNI.`
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            // Send a proper TLS alert before ending the connection
 | 
					 | 
				
			||||||
            // Using "unrecognized_name" (112) alert which is a warning level alert (1) 
 | 
					 | 
				
			||||||
            // that encourages clients to retry with proper SNI
 | 
					 | 
				
			||||||
            const alertData = Buffer.from([
 | 
					 | 
				
			||||||
              0x15,       // Alert record type
 | 
					 | 
				
			||||||
              0x03, 0x03, // TLS 1.2 version
 | 
					 | 
				
			||||||
              0x00, 0x02, // Length
 | 
					 | 
				
			||||||
              0x01,       // Warning alert level (not fatal)
 | 
					 | 
				
			||||||
              0x70        // unrecognized_name alert (code 112)
 | 
					 | 
				
			||||||
            ]);
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
              socket.write(alertData, () => {
 | 
					 | 
				
			||||||
                // Only close the socket after we're sure the alert was sent
 | 
					 | 
				
			||||||
                // Give the alert time to be processed by the client
 | 
					 | 
				
			||||||
                setTimeout(() => {
 | 
					 | 
				
			||||||
                  socket.end();
 | 
					 | 
				
			||||||
                  
 | 
					 | 
				
			||||||
                  // Ensure complete cleanup happens a bit later
 | 
					 | 
				
			||||||
                  setTimeout(() => {
 | 
					 | 
				
			||||||
                    if (!socket.destroyed) {
 | 
					 | 
				
			||||||
                      socket.destroy();
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
 | 
					 | 
				
			||||||
                  }, 100);
 | 
					 | 
				
			||||||
                }, 100);
 | 
					 | 
				
			||||||
              });
 | 
					 | 
				
			||||||
            } catch (err) {
 | 
					 | 
				
			||||||
              // If we can't send the alert, fall back to immediate termination
 | 
					 | 
				
			||||||
              socket.end();
 | 
					 | 
				
			||||||
              this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            if (record.incomingTerminationReason === null) {
 | 
					 | 
				
			||||||
              record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
 | 
					 | 
				
			||||||
              this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni');
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Save domain config and SNI in connection record
 | 
					            // Save domain config and SNI in connection record
 | 
				
			||||||
            const domainConfig = this.domainConfigManager.findDomainConfig(serverName);
 | 
					            const domainConfig = this.domainConfigManager.findDomainConfig(serverName);
 | 
				
			||||||
            record.domainConfig = domainConfig;
 | 
					            record.domainConfig = domainConfig;
 | 
				
			||||||
@@ -247,6 +208,15 @@ export class ConnectionHandler {
 | 
				
			|||||||
              );
 | 
					              );
 | 
				
			||||||
              return;
 | 
					              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`
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Forward directly to NetworkProxy without domain-specific settings
 | 
					        // Forward directly to NetworkProxy without domain-specific settings
 | 
				
			||||||
@@ -260,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);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -385,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 (
 | 
				
			||||||
 | 
					        domainConfig &&
 | 
				
			||||||
        this.domainConfigManager.shouldUseNetworkProxy(domainConfig) &&
 | 
					        this.domainConfigManager.shouldUseNetworkProxy(domainConfig) &&
 | 
				
			||||||
          this.networkProxyBridge.getNetworkProxy()) {
 | 
					        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);
 | 
				
			||||||
@@ -418,19 +383,20 @@ export class ConnectionHandler {
 | 
				
			|||||||
        // 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,
 | 
				
			||||||
@@ -501,9 +467,17 @@ export class ConnectionHandler {
 | 
				
			|||||||
        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}.`
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
@@ -513,9 +487,9 @@ export class ConnectionHandler {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          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(', ')}.`
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -578,54 +552,103 @@ export class ConnectionHandler {
 | 
				
			|||||||
          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.settings.allowSessionTicket === false &&
 | 
				
			||||||
            this.tlsManager.isClientHello(chunk) &&
 | 
					            this.tlsManager.isClientHello(chunk) &&
 | 
				
			||||||
              !serverName) {
 | 
					            !serverName
 | 
				
			||||||
            
 | 
					          ) {
 | 
				
			||||||
            // Always block ClientHello without SNI when allowSessionTicket is false
 | 
					            // 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
 | 
				
			||||||
            // Using "unrecognized_name" (112) alert which is a warning level alert (1) 
 | 
					            if (record.incomingTerminationReason === null) {
 | 
				
			||||||
            // that encourages clients to retry with proper SNI
 | 
					              record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
 | 
				
			||||||
            const alertData = Buffer.from([
 | 
					              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
 | 
					              0x15, // Alert record type
 | 
				
			||||||
              0x03, 0x03, // TLS 1.2 version
 | 
					              0x03,
 | 
				
			||||||
              0x00, 0x02, // Length
 | 
					              0x03, // TLS 1.2 version
 | 
				
			||||||
 | 
					              0x00,
 | 
				
			||||||
 | 
					              0x02, // Length
 | 
				
			||||||
              0x01, // Warning alert level (not fatal)
 | 
					              0x01, // Warning alert level (not fatal)
 | 
				
			||||||
              0x70        // unrecognized_name alert (code 112)
 | 
					              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)
 | 
				
			||||||
            ]);
 | 
					            ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
              socket.write(alertData, () => {
 | 
					              // Use cork/uncork to ensure the alert is sent as a single packet
 | 
				
			||||||
                // Only close the socket after we're sure the alert was sent
 | 
					              socket.cork();
 | 
				
			||||||
                // Give the alert time to be processed by the client
 | 
					              const writeSuccessful = socket.write(closeNotifyAlert);
 | 
				
			||||||
                setTimeout(() => {
 | 
					              socket.uncork();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              // Function to handle the clean socket termination
 | 
				
			||||||
 | 
					              const finishConnection = () => {
 | 
				
			||||||
 | 
					                // First call end() to initiate a graceful close (sends FIN)
 | 
				
			||||||
                socket.end();
 | 
					                socket.end();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  // Ensure complete cleanup happens a bit later
 | 
					                // Allow a short delay for the alert and FIN to be transmitted
 | 
				
			||||||
 | 
					                // before we fully close the socket
 | 
				
			||||||
                setTimeout(() => {
 | 
					                setTimeout(() => {
 | 
				
			||||||
                  if (!socket.destroyed) {
 | 
					                  if (!socket.destroyed) {
 | 
				
			||||||
                    socket.destroy();
 | 
					                    socket.destroy();
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
                  this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
 | 
					                  this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
 | 
				
			||||||
                  }, 100);
 | 
					                }, 150); // Short delay, but longer than the standard TCP ACK timeout
 | 
				
			||||||
                }, 100);
 | 
					              };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (writeSuccessful) {
 | 
				
			||||||
 | 
					                // If the data was successfully written to the kernel buffer,
 | 
				
			||||||
 | 
					                // we can finish the connection after a short delay to ensure transmission
 | 
				
			||||||
 | 
					                setTimeout(finishConnection, 50);
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // If the kernel buffer was full, wait for the drain event
 | 
				
			||||||
 | 
					                socket.once('drain', () => {
 | 
				
			||||||
 | 
					                  setTimeout(finishConnection, 50);
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Set a safety timeout in case drain never happens
 | 
				
			||||||
 | 
					                setTimeout(() => {
 | 
				
			||||||
 | 
					                  socket.removeAllListeners('drain');
 | 
				
			||||||
 | 
					                  finishConnection();
 | 
				
			||||||
 | 
					                }, 250);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
            } catch (err) {
 | 
					            } catch (err) {
 | 
				
			||||||
              // If we can't send the alert, fall back to immediate termination
 | 
					              // 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');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (record.incomingTerminationReason === null) {
 | 
					 | 
				
			||||||
              record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
 | 
					 | 
				
			||||||
              this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni');
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -681,9 +704,7 @@ export class ConnectionHandler {
 | 
				
			|||||||
      : 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 = {
 | 
				
			||||||
@@ -956,7 +977,9 @@ export class ConnectionHandler {
 | 
				
			|||||||
        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
 | 
				
			||||||
@@ -1054,15 +1077,12 @@ export class ConnectionHandler {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Set connection timeout
 | 
					      // Set connection timeout
 | 
				
			||||||
      record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(
 | 
					      record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
 | 
				
			||||||
        record,
 | 
					 | 
				
			||||||
        (record, reason) => {
 | 
					 | 
				
			||||||
        console.log(
 | 
					        console.log(
 | 
				
			||||||
          `[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.`
 | 
					          `[${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) {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user