@push.rocks/smartproxy 18.0.0 → 18.0.2
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 +1 -1
- package/dist_ts/core/utils/route-utils.d.ts +3 -3
- package/dist_ts/core/utils/route-utils.js +9 -9
- package/dist_ts/proxies/network-proxy/http-request-handler.js +3 -2
- package/dist_ts/proxies/nftables-proxy/models/interfaces.d.ts +2 -2
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +21 -21
- package/dist_ts/proxies/smart-proxy/index.d.ts +1 -0
- package/dist_ts/proxies/smart-proxy/index.js +2 -1
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +1 -0
- package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +14 -0
- package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
- package/dist_ts/proxies/smart-proxy/nftables-manager.d.ts +82 -0
- package/dist_ts/proxies/smart-proxy/nftables-manager.js +235 -0
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +42 -1
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +6 -1
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +46 -2
- package/dist_ts/proxies/smart-proxy/utils/index.d.ts +1 -0
- package/dist_ts/proxies/smart-proxy/utils/index.js +3 -2
- package/dist_ts/proxies/smart-proxy/utils/route-helpers.d.ts +77 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +119 -1
- package/package.json +4 -4
- package/readme.plan.md +618 -110
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/utils/route-utils.ts +9 -9
- package/ts/proxies/network-proxy/http-request-handler.ts +3 -2
- package/ts/proxies/nftables-proxy/models/interfaces.ts +2 -2
- package/ts/proxies/nftables-proxy/nftables-proxy.ts +20 -20
- package/ts/proxies/smart-proxy/index.ts +1 -0
- package/ts/proxies/smart-proxy/models/interfaces.ts +3 -0
- package/ts/proxies/smart-proxy/models/route-types.ts +20 -0
- package/ts/proxies/smart-proxy/nftables-manager.ts +268 -0
- package/ts/proxies/smart-proxy/route-connection-handler.ts +55 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +60 -1
- package/ts/proxies/smart-proxy/utils/index.ts +2 -1
- package/ts/proxies/smart-proxy/utils/route-helpers.ts +192 -0
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartproxy',
|
|
6
|
-
version: '18.0.
|
|
6
|
+
version: '18.0.2',
|
|
7
7
|
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
|
8
8
|
}
|
|
@@ -209,18 +209,18 @@ export function matchIpPattern(pattern: string, ip: string): boolean {
|
|
|
209
209
|
* Match an IP against allowed and blocked IP patterns
|
|
210
210
|
*
|
|
211
211
|
* @param ip IP to check
|
|
212
|
-
* @param
|
|
213
|
-
* @param
|
|
212
|
+
* @param ipAllowList Array of allowed IP patterns
|
|
213
|
+
* @param ipBlockList Array of blocked IP patterns
|
|
214
214
|
* @returns Whether the IP is allowed
|
|
215
215
|
*/
|
|
216
216
|
export function isIpAuthorized(
|
|
217
217
|
ip: string,
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
ipAllowList: string[] = ['*'],
|
|
219
|
+
ipBlockList: string[] = []
|
|
220
220
|
): boolean {
|
|
221
221
|
// Check blocked IPs first
|
|
222
|
-
if (
|
|
223
|
-
for (const pattern of
|
|
222
|
+
if (ipBlockList.length > 0) {
|
|
223
|
+
for (const pattern of ipBlockList) {
|
|
224
224
|
if (matchIpPattern(pattern, ip)) {
|
|
225
225
|
return false; // IP is blocked
|
|
226
226
|
}
|
|
@@ -228,13 +228,13 @@ export function isIpAuthorized(
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
// If there are allowed IPs, check them
|
|
231
|
-
if (
|
|
231
|
+
if (ipAllowList.length > 0) {
|
|
232
232
|
// Special case: if '*' is in allowed IPs, all non-blocked IPs are allowed
|
|
233
|
-
if (
|
|
233
|
+
if (ipAllowList.includes('*')) {
|
|
234
234
|
return true;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
for (const pattern of
|
|
237
|
+
for (const pattern of ipAllowList) {
|
|
238
238
|
if (matchIpPattern(pattern, ip)) {
|
|
239
239
|
return true; // IP is allowed
|
|
240
240
|
}
|
|
@@ -41,11 +41,12 @@ export class HttpRequestHandler {
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
// Optionally rewrite host header to match target
|
|
44
|
-
if (options.headers && options.headers
|
|
44
|
+
if (options.headers && 'host' in options.headers) {
|
|
45
45
|
// Only apply if host header rewrite is enabled or not explicitly disabled
|
|
46
46
|
const shouldRewriteHost = route?.action.options?.rewriteHostHeader !== false;
|
|
47
47
|
if (shouldRewriteHost) {
|
|
48
|
-
|
|
48
|
+
// Safely cast to OutgoingHttpHeaders to access host property
|
|
49
|
+
(options.headers as plugins.http.OutgoingHttpHeaders).host = `${destination.host}:${destination.port}`;
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -31,8 +31,8 @@ export interface NfTableProxyOptions {
|
|
|
31
31
|
logFormat?: 'plain' | 'json'; // Format for logs
|
|
32
32
|
|
|
33
33
|
// Source filtering
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
ipAllowList?: string[]; // If provided, only these IPs are allowed
|
|
35
|
+
ipBlockList?: string[]; // If provided, these IPs are blocked
|
|
36
36
|
useIPSets?: boolean; // Use nftables sets for efficient IP management
|
|
37
37
|
|
|
38
38
|
// Rule management
|
|
@@ -134,8 +134,8 @@ export class NfTablesProxy {
|
|
|
134
134
|
}
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
-
validateIPs(settings.
|
|
138
|
-
validateIPs(settings.
|
|
137
|
+
validateIPs(settings.ipAllowList);
|
|
138
|
+
validateIPs(settings.ipBlockList);
|
|
139
139
|
|
|
140
140
|
// Validate toHost - only allow hostnames or IPs
|
|
141
141
|
if (settings.toHost) {
|
|
@@ -426,7 +426,7 @@ export class NfTablesProxy {
|
|
|
426
426
|
* Adds source IP filtering rules, potentially using IP sets for efficiency
|
|
427
427
|
*/
|
|
428
428
|
private async addSourceIPFilters(isIpv6: boolean = false): Promise<boolean> {
|
|
429
|
-
if (!this.settings.
|
|
429
|
+
if (!this.settings.ipAllowList && !this.settings.ipBlockList) {
|
|
430
430
|
return true; // Nothing to do
|
|
431
431
|
}
|
|
432
432
|
|
|
@@ -441,9 +441,9 @@ export class NfTablesProxy {
|
|
|
441
441
|
// Using IP sets for more efficient rule processing with large IP lists
|
|
442
442
|
if (this.settings.useIPSets) {
|
|
443
443
|
// Create sets for banned and allowed IPs if needed
|
|
444
|
-
if (this.settings.
|
|
444
|
+
if (this.settings.ipBlockList && this.settings.ipBlockList.length > 0) {
|
|
445
445
|
const setName = 'banned_ips';
|
|
446
|
-
await this.createIPSet(family, setName, this.settings.
|
|
446
|
+
await this.createIPSet(family, setName, this.settings.ipBlockList, setType as any);
|
|
447
447
|
|
|
448
448
|
// Add rule to drop traffic from banned IPs
|
|
449
449
|
const rule = `add rule ${family} ${this.tableName} ${chain} ip${isIpv6 ? '6' : ''} saddr @${setName} drop comment "${this.ruleTag}:BANNED_SET"`;
|
|
@@ -458,9 +458,9 @@ export class NfTablesProxy {
|
|
|
458
458
|
});
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
-
if (this.settings.
|
|
461
|
+
if (this.settings.ipAllowList && this.settings.ipAllowList.length > 0) {
|
|
462
462
|
const setName = 'allowed_ips';
|
|
463
|
-
await this.createIPSet(family, setName, this.settings.
|
|
463
|
+
await this.createIPSet(family, setName, this.settings.ipAllowList, setType as any);
|
|
464
464
|
|
|
465
465
|
// Add rule to allow traffic from allowed IPs
|
|
466
466
|
const rule = `add rule ${family} ${this.tableName} ${chain} ip${isIpv6 ? '6' : ''} saddr @${setName} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} accept comment "${this.ruleTag}:ALLOWED_SET"`;
|
|
@@ -490,8 +490,8 @@ export class NfTablesProxy {
|
|
|
490
490
|
// Traditional approach without IP sets - less efficient for large IP lists
|
|
491
491
|
|
|
492
492
|
// Ban specific IPs first
|
|
493
|
-
if (this.settings.
|
|
494
|
-
for (const ip of this.settings.
|
|
493
|
+
if (this.settings.ipBlockList && this.settings.ipBlockList.length > 0) {
|
|
494
|
+
for (const ip of this.settings.ipBlockList) {
|
|
495
495
|
// Skip IPv4 addresses for IPv6 rules and vice versa
|
|
496
496
|
if (isIpv6 && ip.includes('.')) continue;
|
|
497
497
|
if (!isIpv6 && ip.includes(':')) continue;
|
|
@@ -510,9 +510,9 @@ export class NfTablesProxy {
|
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
// Allow specific IPs
|
|
513
|
-
if (this.settings.
|
|
513
|
+
if (this.settings.ipAllowList && this.settings.ipAllowList.length > 0) {
|
|
514
514
|
// Add rules to allow specific IPs
|
|
515
|
-
for (const ip of this.settings.
|
|
515
|
+
for (const ip of this.settings.ipAllowList) {
|
|
516
516
|
// Skip IPv4 addresses for IPv6 rules and vice versa
|
|
517
517
|
if (isIpv6 && ip.includes('.')) continue;
|
|
518
518
|
if (!isIpv6 && ip.includes(':')) continue;
|
|
@@ -1398,28 +1398,28 @@ export class NfTablesProxy {
|
|
|
1398
1398
|
|
|
1399
1399
|
// Source IP filters
|
|
1400
1400
|
if (this.settings.useIPSets) {
|
|
1401
|
-
if (this.settings.
|
|
1401
|
+
if (this.settings.ipBlockList?.length) {
|
|
1402
1402
|
commands.push(`add set ip ${this.tableName} banned_ips { type ipv4_addr; }`);
|
|
1403
|
-
commands.push(`add element ip ${this.tableName} banned_ips { ${this.settings.
|
|
1403
|
+
commands.push(`add element ip ${this.tableName} banned_ips { ${this.settings.ipBlockList.join(', ')} }`);
|
|
1404
1404
|
commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr @banned_ips drop comment "${this.ruleTag}:BANNED_SET"`);
|
|
1405
1405
|
}
|
|
1406
1406
|
|
|
1407
|
-
if (this.settings.
|
|
1407
|
+
if (this.settings.ipAllowList?.length) {
|
|
1408
1408
|
commands.push(`add set ip ${this.tableName} allowed_ips { type ipv4_addr; }`);
|
|
1409
|
-
commands.push(`add element ip ${this.tableName} allowed_ips { ${this.settings.
|
|
1409
|
+
commands.push(`add element ip ${this.tableName} allowed_ips { ${this.settings.ipAllowList.join(', ')} }`);
|
|
1410
1410
|
commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr @allowed_ips ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} accept comment "${this.ruleTag}:ALLOWED_SET"`);
|
|
1411
1411
|
commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} drop comment "${this.ruleTag}:DENY_ALL"`);
|
|
1412
1412
|
}
|
|
1413
|
-
} else if (this.settings.
|
|
1413
|
+
} else if (this.settings.ipBlockList?.length || this.settings.ipAllowList?.length) {
|
|
1414
1414
|
// Traditional approach without IP sets
|
|
1415
|
-
if (this.settings.
|
|
1416
|
-
for (const ip of this.settings.
|
|
1415
|
+
if (this.settings.ipBlockList?.length) {
|
|
1416
|
+
for (const ip of this.settings.ipBlockList) {
|
|
1417
1417
|
commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr ${ip} drop comment "${this.ruleTag}:BANNED"`);
|
|
1418
1418
|
}
|
|
1419
1419
|
}
|
|
1420
1420
|
|
|
1421
|
-
if (this.settings.
|
|
1422
|
-
for (const ip of this.settings.
|
|
1421
|
+
if (this.settings.ipAllowList?.length) {
|
|
1422
|
+
for (const ip of this.settings.ipAllowList) {
|
|
1423
1423
|
commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr ${ip} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} accept comment "${this.ruleTag}:ALLOWED"`);
|
|
1424
1424
|
}
|
|
1425
1425
|
commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} drop comment "${this.ruleTag}:DENY_ALL"`);
|
|
@@ -19,6 +19,7 @@ export { NetworkProxyBridge } from './network-proxy-bridge.js';
|
|
|
19
19
|
// Export route-based components
|
|
20
20
|
export { RouteManager } from './route-manager.js';
|
|
21
21
|
export { RouteConnectionHandler } from './route-connection-handler.js';
|
|
22
|
+
export { NFTablesManager } from './nftables-manager.js';
|
|
22
23
|
|
|
23
24
|
// Export all helper functions from the utils directory
|
|
24
25
|
export * from './utils/index.js';
|
|
@@ -142,4 +142,7 @@ export interface IConnectionRecord {
|
|
|
142
142
|
// Browser connection tracking
|
|
143
143
|
isBrowserConnection?: boolean; // Whether this connection appears to be from a browser
|
|
144
144
|
domainSwitches?: number; // Number of times the domain has been switched on this connection
|
|
145
|
+
|
|
146
|
+
// NFTables tracking
|
|
147
|
+
nftablesHandled?: boolean; // Whether this connection is being handled by NFTables at kernel level
|
|
145
148
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../../../plugins.js';
|
|
2
2
|
import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js';
|
|
3
3
|
import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
|
|
4
|
+
import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Supported action types for route configurations
|
|
@@ -259,6 +260,12 @@ export interface IRouteAction {
|
|
|
259
260
|
backendProtocol?: 'http1' | 'http2';
|
|
260
261
|
[key: string]: any;
|
|
261
262
|
};
|
|
263
|
+
|
|
264
|
+
// Forwarding engine specification
|
|
265
|
+
forwardingEngine?: 'node' | 'nftables';
|
|
266
|
+
|
|
267
|
+
// NFTables-specific options
|
|
268
|
+
nftables?: INfTablesOptions;
|
|
262
269
|
}
|
|
263
270
|
|
|
264
271
|
/**
|
|
@@ -275,6 +282,19 @@ export interface IRouteRateLimit {
|
|
|
275
282
|
|
|
276
283
|
// IRouteSecurity is defined above - unified definition is used for all routes
|
|
277
284
|
|
|
285
|
+
/**
|
|
286
|
+
* NFTables-specific configuration options
|
|
287
|
+
*/
|
|
288
|
+
export interface INfTablesOptions {
|
|
289
|
+
preserveSourceIP?: boolean; // Preserve original source IP address
|
|
290
|
+
protocol?: 'tcp' | 'udp' | 'all'; // Protocol to forward
|
|
291
|
+
maxRate?: string; // QoS rate limiting (e.g. "10mbps")
|
|
292
|
+
priority?: number; // QoS priority (1-10, lower is higher priority)
|
|
293
|
+
tableName?: string; // Optional custom table name
|
|
294
|
+
useIPSets?: boolean; // Use IP sets for performance
|
|
295
|
+
useAdvancedNAT?: boolean; // Use connection tracking for stateful NAT
|
|
296
|
+
}
|
|
297
|
+
|
|
278
298
|
/**
|
|
279
299
|
* CORS configuration for a route
|
|
280
300
|
*/
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import { NfTablesProxy } from '../nftables-proxy/nftables-proxy.js';
|
|
3
|
+
import type {
|
|
4
|
+
NfTableProxyOptions,
|
|
5
|
+
PortRange,
|
|
6
|
+
NfTablesStatus
|
|
7
|
+
} from '../nftables-proxy/models/interfaces.js';
|
|
8
|
+
import type {
|
|
9
|
+
IRouteConfig,
|
|
10
|
+
TPortRange,
|
|
11
|
+
INfTablesOptions
|
|
12
|
+
} from './models/route-types.js';
|
|
13
|
+
import type { ISmartProxyOptions } from './models/interfaces.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Manages NFTables rules based on SmartProxy route configurations
|
|
17
|
+
*
|
|
18
|
+
* This class bridges the gap between SmartProxy routes and the NFTablesProxy,
|
|
19
|
+
* allowing high-performance kernel-level packet forwarding for routes that
|
|
20
|
+
* specify NFTables as their forwarding engine.
|
|
21
|
+
*/
|
|
22
|
+
export class NFTablesManager {
|
|
23
|
+
private rulesMap: Map<string, NfTablesProxy> = new Map();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates a new NFTablesManager
|
|
27
|
+
*
|
|
28
|
+
* @param options The SmartProxy options
|
|
29
|
+
*/
|
|
30
|
+
constructor(private options: ISmartProxyOptions) {}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Provision NFTables rules for a route
|
|
34
|
+
*
|
|
35
|
+
* @param route The route configuration
|
|
36
|
+
* @returns A promise that resolves to true if successful, false otherwise
|
|
37
|
+
*/
|
|
38
|
+
public async provisionRoute(route: IRouteConfig): Promise<boolean> {
|
|
39
|
+
// Generate a unique ID for this route
|
|
40
|
+
const routeId = this.generateRouteId(route);
|
|
41
|
+
|
|
42
|
+
// Skip if route doesn't use NFTables
|
|
43
|
+
if (route.action.forwardingEngine !== 'nftables') {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Create NFTables options from route configuration
|
|
48
|
+
const nftOptions = this.createNfTablesOptions(route);
|
|
49
|
+
|
|
50
|
+
// Create and start an NFTablesProxy instance
|
|
51
|
+
const proxy = new NfTablesProxy(nftOptions);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await proxy.start();
|
|
55
|
+
this.rulesMap.set(routeId, proxy);
|
|
56
|
+
return true;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(`Failed to provision NFTables rules for route ${route.name || 'unnamed'}: ${err.message}`);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Remove NFTables rules for a route
|
|
65
|
+
*
|
|
66
|
+
* @param route The route configuration
|
|
67
|
+
* @returns A promise that resolves to true if successful, false otherwise
|
|
68
|
+
*/
|
|
69
|
+
public async deprovisionRoute(route: IRouteConfig): Promise<boolean> {
|
|
70
|
+
const routeId = this.generateRouteId(route);
|
|
71
|
+
|
|
72
|
+
const proxy = this.rulesMap.get(routeId);
|
|
73
|
+
if (!proxy) {
|
|
74
|
+
return true; // Nothing to remove
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await proxy.stop();
|
|
79
|
+
this.rulesMap.delete(routeId);
|
|
80
|
+
return true;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(`Failed to deprovision NFTables rules for route ${route.name || 'unnamed'}: ${err.message}`);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Update NFTables rules when route changes
|
|
89
|
+
*
|
|
90
|
+
* @param oldRoute The previous route configuration
|
|
91
|
+
* @param newRoute The new route configuration
|
|
92
|
+
* @returns A promise that resolves to true if successful, false otherwise
|
|
93
|
+
*/
|
|
94
|
+
public async updateRoute(oldRoute: IRouteConfig, newRoute: IRouteConfig): Promise<boolean> {
|
|
95
|
+
// Remove old rules and add new ones
|
|
96
|
+
await this.deprovisionRoute(oldRoute);
|
|
97
|
+
return this.provisionRoute(newRoute);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Generate a unique ID for a route
|
|
102
|
+
*
|
|
103
|
+
* @param route The route configuration
|
|
104
|
+
* @returns A unique ID string
|
|
105
|
+
*/
|
|
106
|
+
private generateRouteId(route: IRouteConfig): string {
|
|
107
|
+
// Generate a unique ID based on route properties
|
|
108
|
+
// Include the route name, match criteria, and a timestamp
|
|
109
|
+
const matchStr = JSON.stringify({
|
|
110
|
+
ports: route.match.ports,
|
|
111
|
+
domains: route.match.domains
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return `${route.name || 'unnamed'}-${matchStr}-${route.id || Date.now().toString()}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create NFTablesProxy options from a route configuration
|
|
119
|
+
*
|
|
120
|
+
* @param route The route configuration
|
|
121
|
+
* @returns NFTableProxyOptions object
|
|
122
|
+
*/
|
|
123
|
+
private createNfTablesOptions(route: IRouteConfig): NfTableProxyOptions {
|
|
124
|
+
const { action } = route;
|
|
125
|
+
|
|
126
|
+
// Ensure we have a target
|
|
127
|
+
if (!action.target) {
|
|
128
|
+
throw new Error('Route must have a target to use NFTables forwarding');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Convert port specifications
|
|
132
|
+
const fromPorts = this.expandPortRange(route.match.ports);
|
|
133
|
+
|
|
134
|
+
// Determine target port
|
|
135
|
+
let toPorts: number | PortRange | Array<number | PortRange>;
|
|
136
|
+
|
|
137
|
+
if (action.target.port === 'preserve') {
|
|
138
|
+
// 'preserve' means use the same ports as the source
|
|
139
|
+
toPorts = fromPorts;
|
|
140
|
+
} else if (typeof action.target.port === 'function') {
|
|
141
|
+
// For function-based ports, we can't determine at setup time
|
|
142
|
+
// Use the "preserve" approach and let NFTables handle it
|
|
143
|
+
toPorts = fromPorts;
|
|
144
|
+
} else {
|
|
145
|
+
toPorts = action.target.port;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Determine target host
|
|
149
|
+
let toHost: string;
|
|
150
|
+
if (typeof action.target.host === 'function') {
|
|
151
|
+
// Can't determine at setup time, use localhost as a placeholder
|
|
152
|
+
// and rely on run-time handling
|
|
153
|
+
toHost = 'localhost';
|
|
154
|
+
} else if (Array.isArray(action.target.host)) {
|
|
155
|
+
// Use first host for now - NFTables will do simple round-robin
|
|
156
|
+
toHost = action.target.host[0];
|
|
157
|
+
} else {
|
|
158
|
+
toHost = action.target.host;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Create options
|
|
162
|
+
const options: NfTableProxyOptions = {
|
|
163
|
+
fromPort: fromPorts,
|
|
164
|
+
toPort: toPorts,
|
|
165
|
+
toHost: toHost,
|
|
166
|
+
protocol: action.nftables?.protocol || 'tcp',
|
|
167
|
+
preserveSourceIP: action.nftables?.preserveSourceIP !== undefined ?
|
|
168
|
+
action.nftables.preserveSourceIP :
|
|
169
|
+
this.options.preserveSourceIP,
|
|
170
|
+
useIPSets: action.nftables?.useIPSets !== false,
|
|
171
|
+
useAdvancedNAT: action.nftables?.useAdvancedNAT,
|
|
172
|
+
enableLogging: this.options.enableDetailedLogging,
|
|
173
|
+
deleteOnExit: true,
|
|
174
|
+
tableName: action.nftables?.tableName || 'smartproxy'
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Add security-related options
|
|
178
|
+
const security = action.security || route.security;
|
|
179
|
+
if (security?.ipAllowList?.length) {
|
|
180
|
+
options.ipAllowList = security.ipAllowList;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (security?.ipBlockList?.length) {
|
|
184
|
+
options.ipBlockList = security.ipBlockList;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Add QoS options
|
|
188
|
+
if (action.nftables?.maxRate || action.nftables?.priority) {
|
|
189
|
+
options.qos = {
|
|
190
|
+
enabled: true,
|
|
191
|
+
maxRate: action.nftables.maxRate,
|
|
192
|
+
priority: action.nftables.priority
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return options;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Expand port range specifications
|
|
201
|
+
*
|
|
202
|
+
* @param ports The port range specification
|
|
203
|
+
* @returns Expanded port range
|
|
204
|
+
*/
|
|
205
|
+
private expandPortRange(ports: TPortRange): number | PortRange | Array<number | PortRange> {
|
|
206
|
+
// Process different port specifications
|
|
207
|
+
if (typeof ports === 'number') {
|
|
208
|
+
return ports;
|
|
209
|
+
} else if (Array.isArray(ports)) {
|
|
210
|
+
const result: Array<number | PortRange> = [];
|
|
211
|
+
|
|
212
|
+
for (const item of ports) {
|
|
213
|
+
if (typeof item === 'number') {
|
|
214
|
+
result.push(item);
|
|
215
|
+
} else if ('from' in item && 'to' in item) {
|
|
216
|
+
result.push({ from: item.from, to: item.to });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return result;
|
|
221
|
+
} else if (typeof ports === 'object' && ports !== null && 'from' in ports && 'to' in ports) {
|
|
222
|
+
return { from: (ports as any).from, to: (ports as any).to };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Fallback to port 80 if something went wrong
|
|
226
|
+
console.warn('Invalid port range specification, using port 80 as fallback');
|
|
227
|
+
return 80;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get status of all managed rules
|
|
232
|
+
*
|
|
233
|
+
* @returns A promise that resolves to a record of NFTables status objects
|
|
234
|
+
*/
|
|
235
|
+
public async getStatus(): Promise<Record<string, NfTablesStatus>> {
|
|
236
|
+
const result: Record<string, NfTablesStatus> = {};
|
|
237
|
+
|
|
238
|
+
for (const [routeId, proxy] of this.rulesMap.entries()) {
|
|
239
|
+
result[routeId] = await proxy.getStatus();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if a route is currently provisioned
|
|
247
|
+
*
|
|
248
|
+
* @param route The route configuration
|
|
249
|
+
* @returns True if the route is provisioned, false otherwise
|
|
250
|
+
*/
|
|
251
|
+
public isRouteProvisioned(route: IRouteConfig): boolean {
|
|
252
|
+
const routeId = this.generateRouteId(route);
|
|
253
|
+
return this.rulesMap.has(routeId);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Stop all NFTables rules
|
|
258
|
+
*
|
|
259
|
+
* @returns A promise that resolves when all rules have been stopped
|
|
260
|
+
*/
|
|
261
|
+
public async stop(): Promise<void> {
|
|
262
|
+
// Stop all NFTables proxies
|
|
263
|
+
const stopPromises = Array.from(this.rulesMap.values()).map(proxy => proxy.stop());
|
|
264
|
+
await Promise.all(stopPromises);
|
|
265
|
+
|
|
266
|
+
this.rulesMap.clear();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -338,6 +338,22 @@ export class RouteConnectionHandler {
|
|
|
338
338
|
);
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
// Check if this route uses NFTables for forwarding
|
|
342
|
+
if (route.action.forwardingEngine === 'nftables') {
|
|
343
|
+
// For NFTables routes, we don't need to do anything at the application level
|
|
344
|
+
// The packet is forwarded at the kernel level
|
|
345
|
+
|
|
346
|
+
// Log the connection
|
|
347
|
+
console.log(
|
|
348
|
+
`[${connectionId}] Connection forwarded by NFTables: ${record.remoteIP} -> port ${record.localPort}`
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// Just close the socket in our application since it's handled at kernel level
|
|
352
|
+
socket.end();
|
|
353
|
+
this.connectionManager.cleanupConnection(record, 'nftables_handled');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
341
357
|
// Handle the route based on its action type
|
|
342
358
|
switch (route.action.type) {
|
|
343
359
|
case 'forward':
|
|
@@ -368,6 +384,45 @@ export class RouteConnectionHandler {
|
|
|
368
384
|
const connectionId = record.id;
|
|
369
385
|
const action = route.action;
|
|
370
386
|
|
|
387
|
+
// Check if this route uses NFTables for forwarding
|
|
388
|
+
if (action.forwardingEngine === 'nftables') {
|
|
389
|
+
// Log detailed information about NFTables-handled connection
|
|
390
|
+
if (this.settings.enableDetailedLogging) {
|
|
391
|
+
console.log(
|
|
392
|
+
`[${record.id}] Connection forwarded by NFTables (kernel-level): ` +
|
|
393
|
+
`${record.remoteIP}:${socket.remotePort} -> ${socket.localAddress}:${record.localPort}` +
|
|
394
|
+
` (Route: "${route.name || 'unnamed'}", Domain: ${record.lockedDomain || 'n/a'})`
|
|
395
|
+
);
|
|
396
|
+
} else {
|
|
397
|
+
console.log(
|
|
398
|
+
`[${record.id}] NFTables forwarding: ${record.remoteIP} -> port ${record.localPort} (Route: "${route.name || 'unnamed'}")`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Additional NFTables-specific logging if configured
|
|
403
|
+
if (action.nftables) {
|
|
404
|
+
const nftConfig = action.nftables;
|
|
405
|
+
if (this.settings.enableDetailedLogging) {
|
|
406
|
+
console.log(
|
|
407
|
+
`[${record.id}] NFTables config: ` +
|
|
408
|
+
`protocol=${nftConfig.protocol || 'tcp'}, ` +
|
|
409
|
+
`preserveSourceIP=${nftConfig.preserveSourceIP || false}, ` +
|
|
410
|
+
`priority=${nftConfig.priority || 'default'}, ` +
|
|
411
|
+
`maxRate=${nftConfig.maxRate || 'unlimited'}`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// This connection is handled at the kernel level, no need to process at application level
|
|
417
|
+
// Close the socket gracefully in our application layer
|
|
418
|
+
socket.end();
|
|
419
|
+
|
|
420
|
+
// Mark the connection as handled by NFTables for proper cleanup
|
|
421
|
+
record.nftablesHandled = true;
|
|
422
|
+
this.connectionManager.initiateCleanupOnce(record, 'nftables_handled');
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
371
426
|
// We should have a target configuration for forwarding
|
|
372
427
|
if (!action.target) {
|
|
373
428
|
console.log(`[${connectionId}] Forward action missing target configuration`);
|