feat(smartproxy): Implement fallback to NetworkProxy on missing SNI and rename certProvider to certProvisionFunction in CertProvisioner
This commit is contained in:
		| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '10.0.12', | ||||
|   version: '10.1.0', | ||||
|   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.' | ||||
| } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ export class CertProvisioner extends plugins.EventEmitter { | ||||
|   private domainConfigs: IDomainConfig[]; | ||||
|   private port80Handler: Port80Handler; | ||||
|   private networkProxyBridge: NetworkProxyBridge; | ||||
|   private certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>; | ||||
|   private certProvisionFunction?: (domain: string) => Promise<ISmartProxyCertProvisionObject>; | ||||
|   private forwardConfigs: Array<{ domain: string; forwardConfig?: { ip: string; port: number }; acmeForwardConfig?: { ip: string; port: number }; sslRedirect: boolean }>; | ||||
|   private renewThresholdDays: number; | ||||
|   private renewCheckIntervalHours: number; | ||||
| @@ -46,7 +46,7 @@ export class CertProvisioner extends plugins.EventEmitter { | ||||
|     this.domainConfigs = domainConfigs; | ||||
|     this.port80Handler = port80Handler; | ||||
|     this.networkProxyBridge = networkProxyBridge; | ||||
|     this.certProvider = certProvider; | ||||
|     this.certProvisionFunction = certProvider; | ||||
|     this.renewThresholdDays = renewThresholdDays; | ||||
|     this.renewCheckIntervalHours = renewCheckIntervalHours; | ||||
|     this.autoRenew = autoRenew; | ||||
| @@ -83,9 +83,9 @@ export class CertProvisioner extends plugins.EventEmitter { | ||||
|     for (const domain of domains) { | ||||
|       const isWildcard = domain.includes('*'); | ||||
|       let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01'; | ||||
|       if (this.certProvider) { | ||||
|       if (this.certProvisionFunction) { | ||||
|         try { | ||||
|           provision = await this.certProvider(domain); | ||||
|           provision = await this.certProvisionFunction(domain); | ||||
|         } catch (err) { | ||||
|           console.error(`certProvider error for ${domain}:`, err); | ||||
|         } | ||||
| @@ -128,8 +128,8 @@ export class CertProvisioner extends plugins.EventEmitter { | ||||
|             try { | ||||
|               if (type === 'http01') { | ||||
|                 await this.port80Handler.renewCertificate(domain); | ||||
|               } else if (type === 'static' && this.certProvider) { | ||||
|                 const provision2 = await this.certProvider(domain); | ||||
|               } else if (type === 'static' && this.certProvisionFunction) { | ||||
|                 const provision2 = await this.certProvisionFunction(domain); | ||||
|                 if (provision2 !== 'http01') { | ||||
|                   const certObj = provision2 as plugins.tsclass.network.ICert; | ||||
|                   const certData: ICertificateData = { | ||||
| @@ -173,8 +173,8 @@ export class CertProvisioner extends plugins.EventEmitter { | ||||
|     const isWildcard = domain.includes('*'); | ||||
|     // Determine provisioning method | ||||
|     let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01'; | ||||
|     if (this.certProvider) { | ||||
|       provision = await this.certProvider(domain); | ||||
|     if (this.certProvisionFunction) { | ||||
|       provision = await this.certProvisionFunction(domain); | ||||
|     } else if (isWildcard) { | ||||
|       // Cannot perform HTTP-01 on wildcard without certProvider | ||||
|       throw new Error(`Cannot request certificate for wildcard domain without certProvisionFunction: ${domain}`); | ||||
|   | ||||
| @@ -557,13 +557,41 @@ export class ConnectionHandler { | ||||
|             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.` | ||||
|             // Missing SNI: forward to NetworkProxy if available | ||||
|             const proxyInstance = this.networkProxyBridge.getNetworkProxy(); | ||||
|             if (proxyInstance) { | ||||
|               if (this.settings.enableDetailedLogging) { | ||||
|                 console.log( | ||||
|                   `[${connectionId}] No SNI in ClientHello; forwarding to NetworkProxy.` | ||||
|                 ); | ||||
|               } | ||||
|             this.networkProxyBridge.forwardToNetworkProxy( | ||||
|               connectionId, | ||||
|               socket, | ||||
|               record, | ||||
|               chunk, | ||||
|               undefined, | ||||
|               (_reason) => { | ||||
|                 // On proxy failure, send TLS unrecognized_name alert and cleanup | ||||
|                 if (record.incomingTerminationReason === null) { | ||||
|                   record.incomingTerminationReason = 'session_ticket_blocked_no_sni'; | ||||
|                   this.connectionManager.incrementTerminationStat( | ||||
|                     'incoming', | ||||
|                     'session_ticket_blocked_no_sni' | ||||
|                   ); | ||||
|                 } | ||||
|                 const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]); | ||||
|                 try { socket.cork(); socket.write(alert); socket.uncork(); socket.end(); } | ||||
|                 catch { socket.end(); } | ||||
|                 this.connectionManager.initiateCleanupOnce(record, 'session_ticket_blocked_no_sni'); | ||||
|               } | ||||
|             ); | ||||
|               return; | ||||
|             } | ||||
|             // Fallback: send TLS unrecognized_name alert and terminate | ||||
|             console.log( | ||||
|               `[${connectionId}] No SNI detected and proxy unavailable; sending TLS alert.` | ||||
|             ); | ||||
|  | ||||
|             // Set the termination reason first | ||||
|             if (record.incomingTerminationReason === null) { | ||||
|               record.incomingTerminationReason = 'session_ticket_blocked_no_sni'; | ||||
|               this.connectionManager.incrementTerminationStat( | ||||
| @@ -571,54 +599,10 @@ export class ConnectionHandler { | ||||
|                 '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'); | ||||
|             } | ||||
|  | ||||
|             const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]); | ||||
|             try { socket.cork(); socket.write(alert); socket.uncork(); socket.end(); } | ||||
|             catch { socket.end(); } | ||||
|             this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni'); | ||||
|             return; | ||||
|           } | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user