@push.rocks/smartproxy 3.41.7 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/classes.portproxy.js +83 -69
- package/dist_ts/classes.pp.acmemanager.d.ts +34 -0
- package/dist_ts/classes.pp.acmemanager.js +123 -0
- package/dist_ts/classes.pp.connectionhandler.d.ts +39 -0
- package/dist_ts/classes.pp.connectionhandler.js +693 -0
- package/dist_ts/classes.pp.connectionmanager.d.ts +78 -0
- package/dist_ts/classes.pp.connectionmanager.js +378 -0
- package/dist_ts/classes.pp.domainconfigmanager.d.ts +55 -0
- package/dist_ts/classes.pp.domainconfigmanager.js +103 -0
- package/dist_ts/classes.pp.interfaces.d.ts +109 -0
- package/dist_ts/classes.pp.interfaces.js +2 -0
- package/dist_ts/classes.pp.networkproxybridge.d.ts +43 -0
- package/dist_ts/classes.pp.networkproxybridge.js +211 -0
- package/dist_ts/classes.pp.portproxy.d.ts +48 -0
- package/dist_ts/classes.pp.portproxy.js +268 -0
- package/dist_ts/classes.pp.portrangemanager.d.ts +56 -0
- package/dist_ts/classes.pp.portrangemanager.js +179 -0
- package/dist_ts/classes.pp.securitymanager.d.ts +47 -0
- package/dist_ts/classes.pp.securitymanager.js +126 -0
- package/dist_ts/classes.pp.snihandler.d.ts +160 -0
- package/dist_ts/classes.pp.snihandler.js +1073 -0
- package/dist_ts/classes.pp.timeoutmanager.d.ts +47 -0
- package/dist_ts/classes.pp.timeoutmanager.js +154 -0
- package/dist_ts/classes.pp.tlsmanager.d.ts +57 -0
- package/dist_ts/classes.pp.tlsmanager.js +132 -0
- package/dist_ts/index.d.ts +2 -2
- package/dist_ts/index.js +3 -3
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.pp.acmemanager.ts +149 -0
- package/ts/classes.pp.connectionhandler.ts +982 -0
- package/ts/classes.pp.connectionmanager.ts +446 -0
- package/ts/classes.pp.domainconfigmanager.ts +123 -0
- package/ts/classes.pp.interfaces.ts +136 -0
- package/ts/classes.pp.networkproxybridge.ts +258 -0
- package/ts/classes.pp.portproxy.ts +344 -0
- package/ts/classes.pp.portrangemanager.ts +214 -0
- package/ts/classes.pp.securitymanager.ts +147 -0
- package/ts/{classes.snihandler.ts → classes.pp.snihandler.ts} +2 -169
- package/ts/classes.pp.timeoutmanager.ts +190 -0
- package/ts/classes.pp.tlsmanager.ts +206 -0
- package/ts/index.ts +2 -2
- package/ts/classes.portproxy.ts +0 -2496
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type{ IPortProxySettings } from './classes.pp.interfaces.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages port ranges and port-based configuration
|
|
5
|
+
*/
|
|
6
|
+
export class PortRangeManager {
|
|
7
|
+
constructor(private settings: IPortProxySettings) {}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get all ports that should be listened on
|
|
11
|
+
*/
|
|
12
|
+
public getListeningPorts(): Set<number> {
|
|
13
|
+
const listeningPorts = new Set<number>();
|
|
14
|
+
|
|
15
|
+
// Always include the main fromPort
|
|
16
|
+
listeningPorts.add(this.settings.fromPort);
|
|
17
|
+
|
|
18
|
+
// Add ports from global port ranges if defined
|
|
19
|
+
if (this.settings.globalPortRanges && this.settings.globalPortRanges.length > 0) {
|
|
20
|
+
for (const range of this.settings.globalPortRanges) {
|
|
21
|
+
for (let port = range.from; port <= range.to; port++) {
|
|
22
|
+
listeningPorts.add(port);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return listeningPorts;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if a port should use NetworkProxy for forwarding
|
|
32
|
+
*/
|
|
33
|
+
public shouldUseNetworkProxy(port: number): boolean {
|
|
34
|
+
return !!this.settings.useNetworkProxy && this.settings.useNetworkProxy.includes(port);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if port should use global forwarding
|
|
39
|
+
*/
|
|
40
|
+
public shouldUseGlobalForwarding(port: number): boolean {
|
|
41
|
+
return (
|
|
42
|
+
!!this.settings.forwardAllGlobalRanges &&
|
|
43
|
+
this.isPortInGlobalRanges(port)
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a port is in global ranges
|
|
49
|
+
*/
|
|
50
|
+
public isPortInGlobalRanges(port: number): boolean {
|
|
51
|
+
return (
|
|
52
|
+
this.settings.globalPortRanges &&
|
|
53
|
+
this.isPortInRanges(port, this.settings.globalPortRanges)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a port falls within the specified ranges
|
|
59
|
+
*/
|
|
60
|
+
public isPortInRanges(port: number, ranges: Array<{ from: number; to: number }>): boolean {
|
|
61
|
+
return ranges.some((range) => port >= range.from && port <= range.to);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get forwarding port for a specific listening port
|
|
66
|
+
* This determines what port to connect to on the target
|
|
67
|
+
*/
|
|
68
|
+
public getForwardingPort(listeningPort: number): number {
|
|
69
|
+
// If using global forwarding, forward to the original port
|
|
70
|
+
if (this.settings.forwardAllGlobalRanges && this.isPortInGlobalRanges(listeningPort)) {
|
|
71
|
+
return listeningPort;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Otherwise use the configured toPort
|
|
75
|
+
return this.settings.toPort;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Find domain-specific port ranges that include a given port
|
|
80
|
+
*/
|
|
81
|
+
public findDomainPortRange(port: number): {
|
|
82
|
+
domainIndex: number,
|
|
83
|
+
range: { from: number, to: number }
|
|
84
|
+
} | undefined {
|
|
85
|
+
for (let i = 0; i < this.settings.domainConfigs.length; i++) {
|
|
86
|
+
const domain = this.settings.domainConfigs[i];
|
|
87
|
+
if (domain.portRanges) {
|
|
88
|
+
for (const range of domain.portRanges) {
|
|
89
|
+
if (port >= range.from && port <= range.to) {
|
|
90
|
+
return { domainIndex: i, range };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get a list of all configured ports
|
|
100
|
+
* This includes the fromPort, NetworkProxy ports, and ports from all ranges
|
|
101
|
+
*/
|
|
102
|
+
public getAllConfiguredPorts(): number[] {
|
|
103
|
+
const ports = new Set<number>();
|
|
104
|
+
|
|
105
|
+
// Add main listening port
|
|
106
|
+
ports.add(this.settings.fromPort);
|
|
107
|
+
|
|
108
|
+
// Add NetworkProxy port if configured
|
|
109
|
+
if (this.settings.networkProxyPort) {
|
|
110
|
+
ports.add(this.settings.networkProxyPort);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Add NetworkProxy ports
|
|
114
|
+
if (this.settings.useNetworkProxy) {
|
|
115
|
+
for (const port of this.settings.useNetworkProxy) {
|
|
116
|
+
ports.add(port);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Add ACME HTTP challenge port if enabled
|
|
121
|
+
if (this.settings.acme?.enabled && this.settings.acme.port) {
|
|
122
|
+
ports.add(this.settings.acme.port);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Add global port ranges
|
|
126
|
+
if (this.settings.globalPortRanges) {
|
|
127
|
+
for (const range of this.settings.globalPortRanges) {
|
|
128
|
+
for (let port = range.from; port <= range.to; port++) {
|
|
129
|
+
ports.add(port);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add domain-specific port ranges
|
|
135
|
+
for (const domain of this.settings.domainConfigs) {
|
|
136
|
+
if (domain.portRanges) {
|
|
137
|
+
for (const range of domain.portRanges) {
|
|
138
|
+
for (let port = range.from; port <= range.to; port++) {
|
|
139
|
+
ports.add(port);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Add domain-specific NetworkProxy port if configured
|
|
145
|
+
if (domain.useNetworkProxy && domain.networkProxyPort) {
|
|
146
|
+
ports.add(domain.networkProxyPort);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return Array.from(ports);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Validate port configuration
|
|
155
|
+
* Returns array of warning messages
|
|
156
|
+
*/
|
|
157
|
+
public validateConfiguration(): string[] {
|
|
158
|
+
const warnings: string[] = [];
|
|
159
|
+
|
|
160
|
+
// Check for overlapping port ranges
|
|
161
|
+
const portMappings = new Map<number, string[]>();
|
|
162
|
+
|
|
163
|
+
// Track global port ranges
|
|
164
|
+
if (this.settings.globalPortRanges) {
|
|
165
|
+
for (const range of this.settings.globalPortRanges) {
|
|
166
|
+
for (let port = range.from; port <= range.to; port++) {
|
|
167
|
+
if (!portMappings.has(port)) {
|
|
168
|
+
portMappings.set(port, []);
|
|
169
|
+
}
|
|
170
|
+
portMappings.get(port)!.push('Global Port Range');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Track domain-specific port ranges
|
|
176
|
+
for (const domain of this.settings.domainConfigs) {
|
|
177
|
+
if (domain.portRanges) {
|
|
178
|
+
for (const range of domain.portRanges) {
|
|
179
|
+
for (let port = range.from; port <= range.to; port++) {
|
|
180
|
+
if (!portMappings.has(port)) {
|
|
181
|
+
portMappings.set(port, []);
|
|
182
|
+
}
|
|
183
|
+
portMappings.get(port)!.push(`Domain: ${domain.domains.join(', ')}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check for ports with multiple mappings
|
|
190
|
+
for (const [port, mappings] of portMappings.entries()) {
|
|
191
|
+
if (mappings.length > 1) {
|
|
192
|
+
warnings.push(`Port ${port} has multiple mappings: ${mappings.join(', ')}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check if main ports are used elsewhere
|
|
197
|
+
if (portMappings.has(this.settings.fromPort) && portMappings.get(this.settings.fromPort)!.length > 0) {
|
|
198
|
+
warnings.push(`Main listening port ${this.settings.fromPort} is also used in port ranges`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (this.settings.networkProxyPort && portMappings.has(this.settings.networkProxyPort)) {
|
|
202
|
+
warnings.push(`NetworkProxy port ${this.settings.networkProxyPort} is also used in port ranges`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check ACME port
|
|
206
|
+
if (this.settings.acme?.enabled && this.settings.acme.port) {
|
|
207
|
+
if (portMappings.has(this.settings.acme.port)) {
|
|
208
|
+
warnings.push(`ACME HTTP challenge port ${this.settings.acme.port} is also used in port ranges`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return warnings;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IPortProxySettings } from './classes.pp.interfaces.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles security aspects like IP tracking, rate limiting, and authorization
|
|
6
|
+
*/
|
|
7
|
+
export class SecurityManager {
|
|
8
|
+
private connectionsByIP: Map<string, Set<string>> = new Map();
|
|
9
|
+
private connectionRateByIP: Map<string, number[]> = new Map();
|
|
10
|
+
|
|
11
|
+
constructor(private settings: IPortProxySettings) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get connections count by IP
|
|
15
|
+
*/
|
|
16
|
+
public getConnectionCountByIP(ip: string): number {
|
|
17
|
+
return this.connectionsByIP.get(ip)?.size || 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check and update connection rate for an IP
|
|
22
|
+
* @returns true if within rate limit, false if exceeding limit
|
|
23
|
+
*/
|
|
24
|
+
public checkConnectionRate(ip: string): boolean {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const minute = 60 * 1000;
|
|
27
|
+
|
|
28
|
+
if (!this.connectionRateByIP.has(ip)) {
|
|
29
|
+
this.connectionRateByIP.set(ip, [now]);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Get timestamps and filter out entries older than 1 minute
|
|
34
|
+
const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute);
|
|
35
|
+
timestamps.push(now);
|
|
36
|
+
this.connectionRateByIP.set(ip, timestamps);
|
|
37
|
+
|
|
38
|
+
// Check if rate exceeds limit
|
|
39
|
+
return timestamps.length <= this.settings.connectionRateLimitPerMinute!;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Track connection by IP
|
|
44
|
+
*/
|
|
45
|
+
public trackConnectionByIP(ip: string, connectionId: string): void {
|
|
46
|
+
if (!this.connectionsByIP.has(ip)) {
|
|
47
|
+
this.connectionsByIP.set(ip, new Set());
|
|
48
|
+
}
|
|
49
|
+
this.connectionsByIP.get(ip)!.add(connectionId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Remove connection tracking for an IP
|
|
54
|
+
*/
|
|
55
|
+
public removeConnectionByIP(ip: string, connectionId: string): void {
|
|
56
|
+
if (this.connectionsByIP.has(ip)) {
|
|
57
|
+
const connections = this.connectionsByIP.get(ip)!;
|
|
58
|
+
connections.delete(connectionId);
|
|
59
|
+
if (connections.size === 0) {
|
|
60
|
+
this.connectionsByIP.delete(ip);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if an IP is allowed using glob patterns
|
|
67
|
+
*/
|
|
68
|
+
public isIPAuthorized(ip: string, allowedIPs: string[], blockedIPs: string[] = []): boolean {
|
|
69
|
+
// Skip IP validation if allowedIPs is empty
|
|
70
|
+
if (!ip || (allowedIPs.length === 0 && blockedIPs.length === 0)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// First check if IP is blocked
|
|
75
|
+
if (blockedIPs.length > 0 && this.isGlobIPMatch(ip, blockedIPs)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Then check if IP is allowed
|
|
80
|
+
return this.isGlobIPMatch(ip, allowedIPs);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if the IP matches any of the glob patterns
|
|
85
|
+
*/
|
|
86
|
+
private isGlobIPMatch(ip: string, patterns: string[]): boolean {
|
|
87
|
+
if (!ip || !patterns || patterns.length === 0) return false;
|
|
88
|
+
|
|
89
|
+
const normalizeIP = (ip: string): string[] => {
|
|
90
|
+
if (!ip) return [];
|
|
91
|
+
if (ip.startsWith('::ffff:')) {
|
|
92
|
+
const ipv4 = ip.slice(7);
|
|
93
|
+
return [ip, ipv4];
|
|
94
|
+
}
|
|
95
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
|
|
96
|
+
return [ip, `::ffff:${ip}`];
|
|
97
|
+
}
|
|
98
|
+
return [ip];
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const normalizedIPVariants = normalizeIP(ip);
|
|
102
|
+
if (normalizedIPVariants.length === 0) return false;
|
|
103
|
+
|
|
104
|
+
const expandedPatterns = patterns.flatMap(normalizeIP);
|
|
105
|
+
return normalizedIPVariants.some((ipVariant) =>
|
|
106
|
+
expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern))
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if IP should be allowed considering connection rate and max connections
|
|
112
|
+
* @returns Object with result and reason
|
|
113
|
+
*/
|
|
114
|
+
public validateIP(ip: string): { allowed: boolean; reason?: string } {
|
|
115
|
+
// Check connection count limit
|
|
116
|
+
if (
|
|
117
|
+
this.settings.maxConnectionsPerIP &&
|
|
118
|
+
this.getConnectionCountByIP(ip) >= this.settings.maxConnectionsPerIP
|
|
119
|
+
) {
|
|
120
|
+
return {
|
|
121
|
+
allowed: false,
|
|
122
|
+
reason: `Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded`
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check connection rate limit
|
|
127
|
+
if (
|
|
128
|
+
this.settings.connectionRateLimitPerMinute &&
|
|
129
|
+
!this.checkConnectionRate(ip)
|
|
130
|
+
) {
|
|
131
|
+
return {
|
|
132
|
+
allowed: false,
|
|
133
|
+
reason: `Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded`
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { allowed: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Clears all IP tracking data (for shutdown)
|
|
142
|
+
*/
|
|
143
|
+
public clearIPTracking(): void {
|
|
144
|
+
this.connectionsByIP.clear();
|
|
145
|
+
this.connectionRateByIP.clear();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -22,114 +22,6 @@ export class SniHandler {
|
|
|
22
22
|
private static fragmentedBuffers: Map<string, Buffer> = new Map();
|
|
23
23
|
private static fragmentTimeout: number = 1000; // ms to wait for fragments before cleanup
|
|
24
24
|
|
|
25
|
-
// Session tracking for tab reactivation scenarios
|
|
26
|
-
private static sessionCache: Map<
|
|
27
|
-
string,
|
|
28
|
-
{
|
|
29
|
-
sni: string;
|
|
30
|
-
timestamp: number;
|
|
31
|
-
clientRandom?: Buffer;
|
|
32
|
-
}
|
|
33
|
-
> = new Map();
|
|
34
|
-
|
|
35
|
-
// Longer timeout for session cache (24 hours by default)
|
|
36
|
-
private static sessionCacheTimeout: number = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
|
37
|
-
|
|
38
|
-
// Cleanup interval for session cache (run every hour)
|
|
39
|
-
private static sessionCleanupInterval: NodeJS.Timeout | null = null;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Initialize the session cache cleanup mechanism.
|
|
43
|
-
* This should be called during application startup.
|
|
44
|
-
*/
|
|
45
|
-
public static initSessionCacheCleanup(): void {
|
|
46
|
-
if (this.sessionCleanupInterval === null) {
|
|
47
|
-
this.sessionCleanupInterval = setInterval(() => {
|
|
48
|
-
this.cleanupSessionCache();
|
|
49
|
-
}, 60 * 60 * 1000); // Run every hour
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Clean up expired entries from the session cache
|
|
55
|
-
*/
|
|
56
|
-
private static cleanupSessionCache(): void {
|
|
57
|
-
const now = Date.now();
|
|
58
|
-
const expiredKeys: string[] = [];
|
|
59
|
-
|
|
60
|
-
this.sessionCache.forEach((session, key) => {
|
|
61
|
-
if (now - session.timestamp > this.sessionCacheTimeout) {
|
|
62
|
-
expiredKeys.push(key);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
expiredKeys.forEach((key) => {
|
|
67
|
-
this.sessionCache.delete(key);
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Create a client identity key for session tracking
|
|
73
|
-
* Uses source IP and optional client random for uniqueness
|
|
74
|
-
*
|
|
75
|
-
* @param sourceIp - Client IP address
|
|
76
|
-
* @param clientRandom - Optional TLS client random value
|
|
77
|
-
* @returns A string key for the session cache
|
|
78
|
-
*/
|
|
79
|
-
private static createClientKey(sourceIp: string, clientRandom?: Buffer): string {
|
|
80
|
-
if (clientRandom) {
|
|
81
|
-
// If we have the client random, use it for more precise tracking
|
|
82
|
-
return `${sourceIp}:${clientRandom.toString('hex')}`;
|
|
83
|
-
}
|
|
84
|
-
// Fall back to just IP-based tracking
|
|
85
|
-
return sourceIp;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Store SNI information in the session cache
|
|
90
|
-
*
|
|
91
|
-
* @param sourceIp - Client IP address
|
|
92
|
-
* @param sni - The extracted SNI value
|
|
93
|
-
* @param clientRandom - Optional TLS client random value
|
|
94
|
-
*/
|
|
95
|
-
private static cacheSession(sourceIp: string, sni: string, clientRandom?: Buffer): void {
|
|
96
|
-
const key = this.createClientKey(sourceIp, clientRandom);
|
|
97
|
-
this.sessionCache.set(key, {
|
|
98
|
-
sni,
|
|
99
|
-
timestamp: Date.now(),
|
|
100
|
-
clientRandom,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Retrieve SNI information from the session cache
|
|
106
|
-
*
|
|
107
|
-
* @param sourceIp - Client IP address
|
|
108
|
-
* @param clientRandom - Optional TLS client random value
|
|
109
|
-
* @returns The cached SNI or undefined if not found
|
|
110
|
-
*/
|
|
111
|
-
private static getCachedSession(sourceIp: string, clientRandom?: Buffer): string | undefined {
|
|
112
|
-
// Try with client random first for precision
|
|
113
|
-
if (clientRandom) {
|
|
114
|
-
const preciseKey = this.createClientKey(sourceIp, clientRandom);
|
|
115
|
-
const preciseSession = this.sessionCache.get(preciseKey);
|
|
116
|
-
if (preciseSession) {
|
|
117
|
-
return preciseSession.sni;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Fall back to IP-only lookup
|
|
122
|
-
const ipKey = this.createClientKey(sourceIp);
|
|
123
|
-
const session = this.sessionCache.get(ipKey);
|
|
124
|
-
if (session) {
|
|
125
|
-
// Update the timestamp to keep the session alive
|
|
126
|
-
session.timestamp = Date.now();
|
|
127
|
-
return session.sni;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
25
|
/**
|
|
134
26
|
* Extract the client random value from a ClientHello message
|
|
135
27
|
*
|
|
@@ -1172,7 +1064,6 @@ export class SniHandler {
|
|
|
1172
1064
|
* 4. Fragmented ClientHello messages
|
|
1173
1065
|
* 5. TLS 1.3 Early Data (0-RTT)
|
|
1174
1066
|
* 6. Chrome's connection racing behaviors
|
|
1175
|
-
* 7. Tab reactivation patterns with session cache
|
|
1176
1067
|
*
|
|
1177
1068
|
* @param buffer - The buffer containing the TLS ClientHello message
|
|
1178
1069
|
* @param connectionInfo - Optional connection information for fragment handling
|
|
@@ -1235,19 +1126,10 @@ export class SniHandler {
|
|
|
1235
1126
|
const standardSni = this.extractSNI(processBuffer, enableLogging);
|
|
1236
1127
|
if (standardSni) {
|
|
1237
1128
|
log(`Found standard SNI: ${standardSni}`);
|
|
1238
|
-
|
|
1239
|
-
// If we extracted a standard SNI, cache it for future use
|
|
1240
|
-
if (connectionInfo?.sourceIp) {
|
|
1241
|
-
const clientRandom = this.extractClientRandom(processBuffer);
|
|
1242
|
-
this.cacheSession(connectionInfo.sourceIp, standardSni, clientRandom);
|
|
1243
|
-
log(`Cached SNI for future reference: ${standardSni}`);
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
1129
|
return standardSni;
|
|
1247
1130
|
}
|
|
1248
1131
|
|
|
1249
1132
|
// Check for session resumption when standard SNI extraction fails
|
|
1250
|
-
// This may help in chained proxy scenarios
|
|
1251
1133
|
if (this.isClientHello(processBuffer)) {
|
|
1252
1134
|
const resumptionInfo = this.hasSessionResumption(processBuffer, enableLogging);
|
|
1253
1135
|
|
|
@@ -1258,31 +1140,11 @@ export class SniHandler {
|
|
|
1258
1140
|
const pskSni = this.extractSNIFromPSKExtension(processBuffer, enableLogging);
|
|
1259
1141
|
if (pskSni) {
|
|
1260
1142
|
log(`Extracted SNI from PSK extension: ${pskSni}`);
|
|
1261
|
-
|
|
1262
|
-
// Cache this SNI
|
|
1263
|
-
if (connectionInfo?.sourceIp) {
|
|
1264
|
-
const clientRandom = this.extractClientRandom(processBuffer);
|
|
1265
|
-
this.cacheSession(connectionInfo.sourceIp, pskSni, clientRandom);
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
1143
|
return pskSni;
|
|
1269
1144
|
}
|
|
1270
|
-
|
|
1271
|
-
// If session resumption has SNI in a non-standard location,
|
|
1272
|
-
// we need to apply heuristics
|
|
1273
|
-
if (connectionInfo?.sourceIp) {
|
|
1274
|
-
const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
|
1275
|
-
if (cachedSni) {
|
|
1276
|
-
log(`Using cached SNI for session resumption: ${cachedSni}`);
|
|
1277
|
-
return cachedSni;
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
1145
|
}
|
|
1281
1146
|
}
|
|
1282
1147
|
|
|
1283
|
-
// Try tab reactivation and other recovery methods...
|
|
1284
|
-
// (existing code remains unchanged)
|
|
1285
|
-
|
|
1286
1148
|
// Log detailed info about the ClientHello when SNI extraction fails
|
|
1287
1149
|
if (this.isClientHello(processBuffer) && enableLogging) {
|
|
1288
1150
|
log(`SNI extraction failed for ClientHello. Buffer details:`);
|
|
@@ -1303,7 +1165,6 @@ export class SniHandler {
|
|
|
1303
1165
|
}
|
|
1304
1166
|
}
|
|
1305
1167
|
|
|
1306
|
-
// Existing code for fallback methods continues...
|
|
1307
1168
|
return undefined;
|
|
1308
1169
|
}
|
|
1309
1170
|
|
|
@@ -1313,7 +1174,7 @@ export class SniHandler {
|
|
|
1313
1174
|
*
|
|
1314
1175
|
* The method uses connection tracking to handle fragmented ClientHello
|
|
1315
1176
|
* messages and various TLS 1.3 behaviors, including Chrome's connection
|
|
1316
|
-
* racing patterns
|
|
1177
|
+
* racing patterns.
|
|
1317
1178
|
*
|
|
1318
1179
|
* @param buffer - The buffer containing TLS data
|
|
1319
1180
|
* @param connectionInfo - Connection metadata (IPs and ports)
|
|
@@ -1321,7 +1182,6 @@ export class SniHandler {
|
|
|
1321
1182
|
* @param cachedSni - Optional cached SNI from previous connections (for racing detection)
|
|
1322
1183
|
* @returns The extracted server name or undefined if not found or more data needed
|
|
1323
1184
|
*/
|
|
1324
|
-
|
|
1325
1185
|
public static processTlsPacket(
|
|
1326
1186
|
buffer: Buffer,
|
|
1327
1187
|
connectionInfo: {
|
|
@@ -1363,13 +1223,6 @@ export class SniHandler {
|
|
|
1363
1223
|
return cachedSni;
|
|
1364
1224
|
}
|
|
1365
1225
|
|
|
1366
|
-
// Otherwise check our session cache
|
|
1367
|
-
const sessionCachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
|
1368
|
-
if (sessionCachedSni) {
|
|
1369
|
-
log(`Using session-cached SNI for application data: ${sessionCachedSni}`);
|
|
1370
|
-
return sessionCachedSni;
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
1226
|
log('Application data packet without cached SNI, cannot determine hostname');
|
|
1374
1227
|
return undefined;
|
|
1375
1228
|
}
|
|
@@ -1385,9 +1238,6 @@ export class SniHandler {
|
|
|
1385
1238
|
const standardSni = this.extractSNI(buffer, enableLogging);
|
|
1386
1239
|
if (standardSni) {
|
|
1387
1240
|
log(`Found standard SNI in session resumption: ${standardSni}`);
|
|
1388
|
-
|
|
1389
|
-
// Cache this SNI
|
|
1390
|
-
this.cacheSession(connectionInfo.sourceIp, standardSni);
|
|
1391
1241
|
return standardSni;
|
|
1392
1242
|
}
|
|
1393
1243
|
|
|
@@ -1396,7 +1246,6 @@ export class SniHandler {
|
|
|
1396
1246
|
const pskSni = this.extractSNIFromPSKExtension(buffer, enableLogging);
|
|
1397
1247
|
if (pskSni) {
|
|
1398
1248
|
log(`Extracted SNI from PSK extension: ${pskSni}`);
|
|
1399
|
-
this.cacheSession(connectionInfo.sourceIp, pskSni);
|
|
1400
1249
|
return pskSni;
|
|
1401
1250
|
}
|
|
1402
1251
|
|
|
@@ -1430,13 +1279,6 @@ export class SniHandler {
|
|
|
1430
1279
|
}
|
|
1431
1280
|
}
|
|
1432
1281
|
|
|
1433
|
-
// If we still don't have SNI, check for cached sessions
|
|
1434
|
-
const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
|
1435
|
-
if (cachedSni) {
|
|
1436
|
-
log(`Using cached SNI for session resumption: ${cachedSni}`);
|
|
1437
|
-
return cachedSni;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
1282
|
log(`Session resumption without extractable SNI`);
|
|
1441
1283
|
// If allowSessionTicket=false, should be rejected by caller
|
|
1442
1284
|
}
|
|
@@ -1451,19 +1293,10 @@ export class SniHandler {
|
|
|
1451
1293
|
}
|
|
1452
1294
|
|
|
1453
1295
|
// If we couldn't extract an SNI, check if this is a valid ClientHello
|
|
1454
|
-
// If it is, but we couldn't get an SNI, it might be a fragment or
|
|
1455
|
-
// a connection race situation
|
|
1456
1296
|
if (this.isClientHello(buffer)) {
|
|
1457
|
-
// Check if we have a cached session for this IP
|
|
1458
|
-
const sessionCachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
|
1459
|
-
if (sessionCachedSni) {
|
|
1460
|
-
log(`Using session cache for ClientHello without SNI: ${sessionCachedSni}`);
|
|
1461
|
-
return sessionCachedSni;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
1297
|
log('Valid ClientHello detected, but no SNI extracted - might need more data');
|
|
1465
1298
|
}
|
|
1466
1299
|
|
|
1467
1300
|
return undefined;
|
|
1468
1301
|
}
|
|
1469
|
-
}
|
|
1302
|
+
}
|