- Renamed port proxy and SNI handler source files to classes.pp.portproxy.js and classes.pp.snihandler.js respectively - Updated import paths in index.ts and test files (e.g. in test.ts and test.router.ts) to reference the new file names - This refactor improves code organization but breaks direct imports from the old paths
206 lines
5.6 KiB
TypeScript
206 lines
5.6 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
import type { IPortProxySettings } from './classes.pp.interfaces.js';
|
|
import { SniHandler } from './classes.pp.snihandler.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: IPortProxySettings) {}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
} |