update
This commit is contained in:
206
ts/proxies/smart-proxy/tls-manager.ts
Normal file
206
ts/proxies/smart-proxy/tls-manager.ts
Normal file
@ -0,0 +1,206 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { SmartProxyOptions } from './models/interfaces.js';
|
||||
import { SniHandler } from '../../tls/sni/sni-handler.js';
|
||||
|
||||
/**
|
||||
* Interface for connection information used for SNI extraction
|
||||
*/
|
||||
interface IConnectionInfo {
|
||||
sourceIp: string;
|
||||
sourcePort: number;
|
||||
destIp: string;
|
||||
destPort: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages TLS-related operations including SNI extraction and validation
|
||||
*/
|
||||
export class TlsManager {
|
||||
constructor(private settings: SmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Check if a data chunk appears to be a TLS handshake
|
||||
*/
|
||||
public isTlsHandshake(chunk: Buffer): boolean {
|
||||
return SniHandler.isTlsHandshake(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a data chunk appears to be a TLS ClientHello
|
||||
*/
|
||||
public isClientHello(chunk: Buffer): boolean {
|
||||
return SniHandler.isClientHello(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Server Name Indication (SNI) from TLS handshake
|
||||
*/
|
||||
public extractSNI(
|
||||
chunk: Buffer,
|
||||
connInfo: IConnectionInfo,
|
||||
previousDomain?: string
|
||||
): string | undefined {
|
||||
// Use the SniHandler to process the TLS packet
|
||||
return SniHandler.processTlsPacket(
|
||||
chunk,
|
||||
connInfo,
|
||||
this.settings.enableTlsDebugLogging || false,
|
||||
previousDomain
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle session resumption attempts
|
||||
*/
|
||||
public handleSessionResumption(
|
||||
chunk: Buffer,
|
||||
connectionId: string,
|
||||
hasSNI: boolean
|
||||
): { shouldBlock: boolean; reason?: string } {
|
||||
// Skip if session tickets are allowed
|
||||
if (this.settings.allowSessionTicket !== false) {
|
||||
return { shouldBlock: false };
|
||||
}
|
||||
|
||||
// Check for session resumption attempt
|
||||
const resumptionInfo = SniHandler.hasSessionResumption(
|
||||
chunk,
|
||||
this.settings.enableTlsDebugLogging || false
|
||||
);
|
||||
|
||||
// If this is a resumption attempt without SNI, block it
|
||||
if (resumptionInfo.isResumption && !hasSNI && !resumptionInfo.hasSNI) {
|
||||
if (this.settings.enableTlsDebugLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Session resumption detected without SNI and allowSessionTicket=false. ` +
|
||||
`Terminating connection to force new TLS handshake.`
|
||||
);
|
||||
}
|
||||
return {
|
||||
shouldBlock: true,
|
||||
reason: 'session_ticket_blocked'
|
||||
};
|
||||
}
|
||||
|
||||
return { shouldBlock: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for SNI mismatch during renegotiation
|
||||
*/
|
||||
public checkRenegotiationSNI(
|
||||
chunk: Buffer,
|
||||
connInfo: IConnectionInfo,
|
||||
expectedDomain: string,
|
||||
connectionId: string
|
||||
): { hasMismatch: boolean; extractedSNI?: string } {
|
||||
// Only process if this looks like a TLS ClientHello
|
||||
if (!this.isClientHello(chunk)) {
|
||||
return { hasMismatch: false };
|
||||
}
|
||||
|
||||
try {
|
||||
// Extract SNI with renegotiation support
|
||||
const newSNI = SniHandler.extractSNIWithResumptionSupport(
|
||||
chunk,
|
||||
connInfo,
|
||||
this.settings.enableTlsDebugLogging || false
|
||||
);
|
||||
|
||||
// Skip if no SNI was found
|
||||
if (!newSNI) return { hasMismatch: false };
|
||||
|
||||
// Check for SNI mismatch
|
||||
if (newSNI !== expectedDomain) {
|
||||
if (this.settings.enableTlsDebugLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Renegotiation with different SNI: ${expectedDomain} -> ${newSNI}. ` +
|
||||
`Terminating connection - SNI domain switching is not allowed.`
|
||||
);
|
||||
}
|
||||
return { hasMismatch: true, extractedSNI: newSNI };
|
||||
} else if (this.settings.enableTlsDebugLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(
|
||||
`[${connectionId}] Error processing ClientHello: ${err}. Allowing connection to continue.`
|
||||
);
|
||||
}
|
||||
|
||||
return { hasMismatch: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a renegotiation handler function for a connection
|
||||
*/
|
||||
public createRenegotiationHandler(
|
||||
connectionId: string,
|
||||
lockedDomain: string,
|
||||
connInfo: IConnectionInfo,
|
||||
onMismatch: (connectionId: string, reason: string) => void
|
||||
): (chunk: Buffer) => void {
|
||||
return (chunk: Buffer) => {
|
||||
const result = this.checkRenegotiationSNI(chunk, connInfo, lockedDomain, connectionId);
|
||||
if (result.hasMismatch) {
|
||||
onMismatch(connectionId, 'sni_mismatch');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze TLS connection for browser fingerprinting
|
||||
* This helps identify browser vs non-browser connections
|
||||
*/
|
||||
public analyzeClientHello(chunk: Buffer): {
|
||||
isBrowserConnection: boolean;
|
||||
isRenewal: boolean;
|
||||
hasSNI: boolean;
|
||||
} {
|
||||
// Default result
|
||||
const result = {
|
||||
isBrowserConnection: false,
|
||||
isRenewal: false,
|
||||
hasSNI: false
|
||||
};
|
||||
|
||||
try {
|
||||
// Check if it's a ClientHello
|
||||
if (!this.isClientHello(chunk)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check for session resumption
|
||||
const resumptionInfo = SniHandler.hasSessionResumption(
|
||||
chunk,
|
||||
this.settings.enableTlsDebugLogging || false
|
||||
);
|
||||
|
||||
// Extract SNI
|
||||
const sni = SniHandler.extractSNI(
|
||||
chunk,
|
||||
this.settings.enableTlsDebugLogging || false
|
||||
);
|
||||
|
||||
// Update result
|
||||
result.isRenewal = resumptionInfo.isResumption;
|
||||
result.hasSNI = !!sni;
|
||||
|
||||
// Browsers typically:
|
||||
// 1. Send SNI extension
|
||||
// 2. Have a variety of extensions (ALPN, etc.)
|
||||
// 3. Use standard cipher suites
|
||||
// ...more complex heuristics could be implemented here
|
||||
|
||||
// Simple heuristic: presence of SNI suggests browser
|
||||
result.isBrowserConnection = !!sni;
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.log(`Error analyzing ClientHello: ${err}`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user