|
|
|
@ -22,6 +22,114 @@ export class SniHandler {
|
|
|
|
|
private static fragmentedBuffers: Map<string, Buffer> = new Map();
|
|
|
|
|
private static fragmentTimeout: number = 1000; // ms to wait for fragments before cleanup
|
|
|
|
|
|
|
|
|
|
// Session tracking for tab reactivation scenarios
|
|
|
|
|
private static sessionCache: Map<
|
|
|
|
|
string,
|
|
|
|
|
{
|
|
|
|
|
sni: string;
|
|
|
|
|
timestamp: number;
|
|
|
|
|
clientRandom?: Buffer;
|
|
|
|
|
}
|
|
|
|
|
> = new Map();
|
|
|
|
|
|
|
|
|
|
// Longer timeout for session cache (24 hours by default)
|
|
|
|
|
private static sessionCacheTimeout: number = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
|
|
|
|
|
|
|
|
|
// Cleanup interval for session cache (run every hour)
|
|
|
|
|
private static sessionCleanupInterval: NodeJS.Timeout | null = null;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize the session cache cleanup mechanism.
|
|
|
|
|
* This should be called during application startup.
|
|
|
|
|
*/
|
|
|
|
|
public static initSessionCacheCleanup(): void {
|
|
|
|
|
if (this.sessionCleanupInterval === null) {
|
|
|
|
|
this.sessionCleanupInterval = setInterval(() => {
|
|
|
|
|
this.cleanupSessionCache();
|
|
|
|
|
}, 60 * 60 * 1000); // Run every hour
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clean up expired entries from the session cache
|
|
|
|
|
*/
|
|
|
|
|
private static cleanupSessionCache(): void {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const expiredKeys: string[] = [];
|
|
|
|
|
|
|
|
|
|
this.sessionCache.forEach((session, key) => {
|
|
|
|
|
if (now - session.timestamp > this.sessionCacheTimeout) {
|
|
|
|
|
expiredKeys.push(key);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expiredKeys.forEach((key) => {
|
|
|
|
|
this.sessionCache.delete(key);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a client identity key for session tracking
|
|
|
|
|
* Uses source IP and optional client random for uniqueness
|
|
|
|
|
*
|
|
|
|
|
* @param sourceIp - Client IP address
|
|
|
|
|
* @param clientRandom - Optional TLS client random value
|
|
|
|
|
* @returns A string key for the session cache
|
|
|
|
|
*/
|
|
|
|
|
private static createClientKey(sourceIp: string, clientRandom?: Buffer): string {
|
|
|
|
|
if (clientRandom) {
|
|
|
|
|
// If we have the client random, use it for more precise tracking
|
|
|
|
|
return `${sourceIp}:${clientRandom.toString('hex')}`;
|
|
|
|
|
}
|
|
|
|
|
// Fall back to just IP-based tracking
|
|
|
|
|
return sourceIp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Store SNI information in the session cache
|
|
|
|
|
*
|
|
|
|
|
* @param sourceIp - Client IP address
|
|
|
|
|
* @param sni - The extracted SNI value
|
|
|
|
|
* @param clientRandom - Optional TLS client random value
|
|
|
|
|
*/
|
|
|
|
|
private static cacheSession(sourceIp: string, sni: string, clientRandom?: Buffer): void {
|
|
|
|
|
const key = this.createClientKey(sourceIp, clientRandom);
|
|
|
|
|
this.sessionCache.set(key, {
|
|
|
|
|
sni,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
clientRandom,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieve SNI information from the session cache
|
|
|
|
|
*
|
|
|
|
|
* @param sourceIp - Client IP address
|
|
|
|
|
* @param clientRandom - Optional TLS client random value
|
|
|
|
|
* @returns The cached SNI or undefined if not found
|
|
|
|
|
*/
|
|
|
|
|
private static getCachedSession(sourceIp: string, clientRandom?: Buffer): string | undefined {
|
|
|
|
|
// Try with client random first for precision
|
|
|
|
|
if (clientRandom) {
|
|
|
|
|
const preciseKey = this.createClientKey(sourceIp, clientRandom);
|
|
|
|
|
const preciseSession = this.sessionCache.get(preciseKey);
|
|
|
|
|
if (preciseSession) {
|
|
|
|
|
return preciseSession.sni;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fall back to IP-only lookup
|
|
|
|
|
const ipKey = this.createClientKey(sourceIp);
|
|
|
|
|
const session = this.sessionCache.get(ipKey);
|
|
|
|
|
if (session) {
|
|
|
|
|
// Update the timestamp to keep the session alive
|
|
|
|
|
session.timestamp = Date.now();
|
|
|
|
|
return session.sni;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract the client random value from a ClientHello message
|
|
|
|
|
*
|
|
|
|
@ -1064,6 +1172,7 @@ export class SniHandler {
|
|
|
|
|
* 4. Fragmented ClientHello messages
|
|
|
|
|
* 5. TLS 1.3 Early Data (0-RTT)
|
|
|
|
|
* 6. Chrome's connection racing behaviors
|
|
|
|
|
* 7. Tab reactivation patterns with session cache
|
|
|
|
|
*
|
|
|
|
|
* @param buffer - The buffer containing the TLS ClientHello message
|
|
|
|
|
* @param connectionInfo - Optional connection information for fragment handling
|
|
|
|
@ -1126,10 +1235,19 @@ export class SniHandler {
|
|
|
|
|
const standardSni = this.extractSNI(processBuffer, enableLogging);
|
|
|
|
|
if (standardSni) {
|
|
|
|
|
log(`Found standard SNI: ${standardSni}`);
|
|
|
|
|
|
|
|
|
|
// If we extracted a standard SNI, cache it for future use
|
|
|
|
|
if (connectionInfo?.sourceIp) {
|
|
|
|
|
const clientRandom = this.extractClientRandom(processBuffer);
|
|
|
|
|
this.cacheSession(connectionInfo.sourceIp, standardSni, clientRandom);
|
|
|
|
|
log(`Cached SNI for future reference: ${standardSni}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return standardSni;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for session resumption when standard SNI extraction fails
|
|
|
|
|
// This may help in chained proxy scenarios
|
|
|
|
|
if (this.isClientHello(processBuffer)) {
|
|
|
|
|
const resumptionInfo = this.hasSessionResumption(processBuffer, enableLogging);
|
|
|
|
|
|
|
|
|
@ -1140,10 +1258,30 @@ export class SniHandler {
|
|
|
|
|
const pskSni = this.extractSNIFromPSKExtension(processBuffer, enableLogging);
|
|
|
|
|
if (pskSni) {
|
|
|
|
|
log(`Extracted SNI from PSK extension: ${pskSni}`);
|
|
|
|
|
|
|
|
|
|
// Cache this SNI
|
|
|
|
|
if (connectionInfo?.sourceIp) {
|
|
|
|
|
const clientRandom = this.extractClientRandom(processBuffer);
|
|
|
|
|
this.cacheSession(connectionInfo.sourceIp, pskSni, clientRandom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pskSni;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If session resumption has SNI in a non-standard location,
|
|
|
|
|
// we need to apply heuristics
|
|
|
|
|
if (connectionInfo?.sourceIp) {
|
|
|
|
|
const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
|
|
|
|
if (cachedSni) {
|
|
|
|
|
log(`Using cached SNI for session resumption: ${cachedSni}`);
|
|
|
|
|
return cachedSni;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try tab reactivation and other recovery methods...
|
|
|
|
|
// (existing code remains unchanged)
|
|
|
|
|
|
|
|
|
|
// Log detailed info about the ClientHello when SNI extraction fails
|
|
|
|
|
if (this.isClientHello(processBuffer) && enableLogging) {
|
|
|
|
@ -1165,6 +1303,7 @@ export class SniHandler {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Existing code for fallback methods continues...
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -1174,7 +1313,7 @@ export class SniHandler {
|
|
|
|
|
*
|
|
|
|
|
* The method uses connection tracking to handle fragmented ClientHello
|
|
|
|
|
* messages and various TLS 1.3 behaviors, including Chrome's connection
|
|
|
|
|
* racing patterns.
|
|
|
|
|
* racing patterns and tab reactivation behaviors.
|
|
|
|
|
*
|
|
|
|
|
* @param buffer - The buffer containing TLS data
|
|
|
|
|
* @param connectionInfo - Connection metadata (IPs and ports)
|
|
|
|
@ -1182,6 +1321,7 @@ export class SniHandler {
|
|
|
|
|
* @param cachedSni - Optional cached SNI from previous connections (for racing detection)
|
|
|
|
|
* @returns The extracted server name or undefined if not found or more data needed
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
public static processTlsPacket(
|
|
|
|
|
buffer: Buffer,
|
|
|
|
|
connectionInfo: {
|
|
|
|
@ -1223,6 +1363,13 @@ export class SniHandler {
|
|
|
|
|
return cachedSni;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Otherwise check our session cache
|
|
|
|
|
const sessionCachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
|
|
|
|
if (sessionCachedSni) {
|
|
|
|
|
log(`Using session-cached SNI for application data: ${sessionCachedSni}`);
|
|
|
|
|
return sessionCachedSni;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log('Application data packet without cached SNI, cannot determine hostname');
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
@ -1238,6 +1385,9 @@ export class SniHandler {
|
|
|
|
|
const standardSni = this.extractSNI(buffer, enableLogging);
|
|
|
|
|
if (standardSni) {
|
|
|
|
|
log(`Found standard SNI in session resumption: ${standardSni}`);
|
|
|
|
|
|
|
|
|
|
// Cache this SNI
|
|
|
|
|
this.cacheSession(connectionInfo.sourceIp, standardSni);
|
|
|
|
|
return standardSni;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -1246,6 +1396,7 @@ export class SniHandler {
|
|
|
|
|
const pskSni = this.extractSNIFromPSKExtension(buffer, enableLogging);
|
|
|
|
|
if (pskSni) {
|
|
|
|
|
log(`Extracted SNI from PSK extension: ${pskSni}`);
|
|
|
|
|
this.cacheSession(connectionInfo.sourceIp, pskSni);
|
|
|
|
|
return pskSni;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -1279,6 +1430,13 @@ export class SniHandler {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we still don't have SNI, check for cached sessions
|
|
|
|
|
const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
|
|
|
|
if (cachedSni) {
|
|
|
|
|
log(`Using cached SNI for session resumption: ${cachedSni}`);
|
|
|
|
|
return cachedSni;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log(`Session resumption without extractable SNI`);
|
|
|
|
|
// If allowSessionTicket=false, should be rejected by caller
|
|
|
|
|
}
|
|
|
|
@ -1293,7 +1451,16 @@ export class SniHandler {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we couldn't extract an SNI, check if this is a valid ClientHello
|
|
|
|
|
// If it is, but we couldn't get an SNI, it might be a fragment or
|
|
|
|
|
// a connection race situation
|
|
|
|
|
if (this.isClientHello(buffer)) {
|
|
|
|
|
// Check if we have a cached session for this IP
|
|
|
|
|
const sessionCachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
|
|
|
|
if (sessionCachedSni) {
|
|
|
|
|
log(`Using session cache for ClientHello without SNI: ${sessionCachedSni}`);
|
|
|
|
|
return sessionCachedSni;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log('Valid ClientHello detected, but no SNI extracted - might need more data');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|