feat(smartproxy): Implement fallback to NetworkProxy on missing SNI and rename certProvider to certProvisionFunction in CertProvisioner
This commit is contained in:
parent
a9963f3b8a
commit
9c05f71cd6
@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-05-05 - 10.1.0 - feat(smartproxy)
|
||||
Implement fallback to NetworkProxy on missing SNI and rename certProvider to certProvisionFunction in CertProvisioner
|
||||
|
||||
- When a TLS ClientHello is received without an SNI extension and allowSessionTicket is false, the code now attempts to forward the connection to NetworkProxy instead of immediately closing the connection with a TLS alert.
|
||||
- An error callback has been added to handle proxy forwarding failures; if forwarding fails or no NetworkProxy is available, the TLS unrecognized_name alert is sent and the connection is terminated.
|
||||
- Renamed all instances of 'certProvider' to 'certProvisionFunction' in the CertProvisioner implementation, updating the associated types and call sites.
|
||||
- Updated unit tests to simulate a ClientHello without SNI and to verify that with NetworkProxy enabled the connection is correctly forwarded.
|
||||
|
||||
## 2025-05-05 - 10.0.12 - fix(port80handler)
|
||||
refactor ACME challenge handling to use dedicated Http01MemoryHandler, remove obsolete readme.plan.md, and update version to 10.0.12
|
||||
|
||||
|
@ -0,0 +1,18 @@
|
||||
# Plan: Fallback to NetworkProxy on Missing SNI
|
||||
|
||||
When a TLS ClientHello arrives without an SNI extension, we currently send a TLS alert and close the connection. Instead, if NetworkProxy is in use, we want to forward the connection to the network proxy first, and only issue the TLS error if proxying is not possible.
|
||||
|
||||
## Goals
|
||||
- Allow TLS connections with no SNI to be forwarded to NetworkProxy when configured for any domain.
|
||||
- Only send a TLS unrecognized_name alert if proxy forwarding is unavailable or fails.
|
||||
|
||||
## Plan
|
||||
- [ ] In `ts/smartproxy/classes.pp.connectionhandler.ts`, locate the SNI-block branch in `handleStandardConnection` that checks `allowSessionTicket === false && isClientHello && !serverName`.
|
||||
- [ ] Replace the direct TLS alert/error logic with:
|
||||
- If NetworkProxy is enabled (global `useNetworkProxy` setting or an active NetworkProxy instance), call `this.handleNetworkProxyConnection(socket, record)` (passing the buffered ClientHello) before issuing a TLS alert.
|
||||
- Supply an error callback to `forwardToNetworkProxy`; if proxying fails or no NetworkProxy is available, fall back to the original TLS alert sequence.
|
||||
- [ ] Ensure existing metrics and cleanup (`record.incomingTerminationReason`, termination stats) are correctly tracked in the forwarding error path.
|
||||
- [ ] Add or update unit tests to simulate a TLS ClientHello without SNI on port 443 and verify:
|
||||
- When NetworkProxy is enabled, the connection is forwarded to the proxy.
|
||||
- If proxy forwarding fails or the domain is not configured, a TLS alert is sent and the socket is closed.
|
||||
- [ ] Run `pnpm test` to confirm no regressions and that the new behavior is correctly covered.
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user