@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.
Files changed (35) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/core/utils/route-utils.d.ts +3 -3
  3. package/dist_ts/core/utils/route-utils.js +9 -9
  4. package/dist_ts/proxies/network-proxy/http-request-handler.js +3 -2
  5. package/dist_ts/proxies/nftables-proxy/models/interfaces.d.ts +2 -2
  6. package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +21 -21
  7. package/dist_ts/proxies/smart-proxy/index.d.ts +1 -0
  8. package/dist_ts/proxies/smart-proxy/index.js +2 -1
  9. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +1 -0
  10. package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +14 -0
  11. package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
  12. package/dist_ts/proxies/smart-proxy/nftables-manager.d.ts +82 -0
  13. package/dist_ts/proxies/smart-proxy/nftables-manager.js +235 -0
  14. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +42 -1
  15. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +6 -1
  16. package/dist_ts/proxies/smart-proxy/smart-proxy.js +46 -2
  17. package/dist_ts/proxies/smart-proxy/utils/index.d.ts +1 -0
  18. package/dist_ts/proxies/smart-proxy/utils/index.js +3 -2
  19. package/dist_ts/proxies/smart-proxy/utils/route-helpers.d.ts +77 -0
  20. package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +119 -1
  21. package/package.json +4 -4
  22. package/readme.plan.md +618 -110
  23. package/ts/00_commitinfo_data.ts +1 -1
  24. package/ts/core/utils/route-utils.ts +9 -9
  25. package/ts/proxies/network-proxy/http-request-handler.ts +3 -2
  26. package/ts/proxies/nftables-proxy/models/interfaces.ts +2 -2
  27. package/ts/proxies/nftables-proxy/nftables-proxy.ts +20 -20
  28. package/ts/proxies/smart-proxy/index.ts +1 -0
  29. package/ts/proxies/smart-proxy/models/interfaces.ts +3 -0
  30. package/ts/proxies/smart-proxy/models/route-types.ts +20 -0
  31. package/ts/proxies/smart-proxy/nftables-manager.ts +268 -0
  32. package/ts/proxies/smart-proxy/route-connection-handler.ts +55 -0
  33. package/ts/proxies/smart-proxy/smart-proxy.ts +60 -1
  34. package/ts/proxies/smart-proxy/utils/index.ts +2 -1
  35. package/ts/proxies/smart-proxy/utils/route-helpers.ts +192 -0
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartproxy',
6
- version: '18.0.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 allowedIps Array of allowed IP patterns
213
- * @param blockedIps Array of blocked IP patterns
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
- allowedIps: string[] = ['*'],
219
- blockedIps: string[] = []
218
+ ipAllowList: string[] = ['*'],
219
+ ipBlockList: string[] = []
220
220
  ): boolean {
221
221
  // Check blocked IPs first
222
- if (blockedIps.length > 0) {
223
- for (const pattern of blockedIps) {
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 (allowedIps.length > 0) {
231
+ if (ipAllowList.length > 0) {
232
232
  // Special case: if '*' is in allowed IPs, all non-blocked IPs are allowed
233
- if (allowedIps.includes('*')) {
233
+ if (ipAllowList.includes('*')) {
234
234
  return true;
235
235
  }
236
236
 
237
- for (const pattern of allowedIps) {
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.host) {
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
- options.headers.host = `${destination.host}:${destination.port}`;
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
- allowedSourceIPs?: string[]; // If provided, only these IPs are allowed
35
- bannedSourceIPs?: string[]; // If provided, these IPs are blocked
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.allowedSourceIPs);
138
- validateIPs(settings.bannedSourceIPs);
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.allowedSourceIPs && !this.settings.bannedSourceIPs) {
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.bannedSourceIPs && this.settings.bannedSourceIPs.length > 0) {
444
+ if (this.settings.ipBlockList && this.settings.ipBlockList.length > 0) {
445
445
  const setName = 'banned_ips';
446
- await this.createIPSet(family, setName, this.settings.bannedSourceIPs, setType as any);
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.allowedSourceIPs && this.settings.allowedSourceIPs.length > 0) {
461
+ if (this.settings.ipAllowList && this.settings.ipAllowList.length > 0) {
462
462
  const setName = 'allowed_ips';
463
- await this.createIPSet(family, setName, this.settings.allowedSourceIPs, setType as any);
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.bannedSourceIPs && this.settings.bannedSourceIPs.length > 0) {
494
- for (const ip of this.settings.bannedSourceIPs) {
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.allowedSourceIPs && this.settings.allowedSourceIPs.length > 0) {
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.allowedSourceIPs) {
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.bannedSourceIPs?.length) {
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.bannedSourceIPs.join(', ')} }`);
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.allowedSourceIPs?.length) {
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.allowedSourceIPs.join(', ')} }`);
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.bannedSourceIPs?.length || this.settings.allowedSourceIPs?.length) {
1413
+ } else if (this.settings.ipBlockList?.length || this.settings.ipAllowList?.length) {
1414
1414
  // Traditional approach without IP sets
1415
- if (this.settings.bannedSourceIPs?.length) {
1416
- for (const ip of this.settings.bannedSourceIPs) {
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.allowedSourceIPs?.length) {
1422
- for (const ip of this.settings.allowedSourceIPs) {
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`);