@push.rocks/smartproxy 4.3.0 → 5.1.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.
@@ -0,0 +1,2045 @@
1
+ import { exec, execSync } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import * as os from 'os';
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ /**
10
+ * Custom error classes for better error handling
11
+ */
12
+ export class NftBaseError extends Error {
13
+ constructor(message: string) {
14
+ super(message);
15
+ this.name = 'NftBaseError';
16
+ }
17
+ }
18
+
19
+ export class NftValidationError extends NftBaseError {
20
+ constructor(message: string) {
21
+ super(message);
22
+ this.name = 'NftValidationError';
23
+ }
24
+ }
25
+
26
+ export class NftExecutionError extends NftBaseError {
27
+ constructor(message: string) {
28
+ super(message);
29
+ this.name = 'NftExecutionError';
30
+ }
31
+ }
32
+
33
+ export class NftResourceError extends NftBaseError {
34
+ constructor(message: string) {
35
+ super(message);
36
+ this.name = 'NftResourceError';
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Represents a port range for forwarding
42
+ */
43
+ export interface IPortRange {
44
+ from: number;
45
+ to: number;
46
+ }
47
+
48
+ /**
49
+ * Settings for NfTablesProxy.
50
+ */
51
+ export interface INfTableProxySettings {
52
+ // Basic settings
53
+ fromPort: number | IPortRange | Array<number | IPortRange>; // Support single port, port range, or multiple ports/ranges
54
+ toPort: number | IPortRange | Array<number | IPortRange>;
55
+ toHost?: string; // Target host for proxying; defaults to 'localhost'
56
+
57
+ // Advanced settings
58
+ preserveSourceIP?: boolean; // If true, the original source IP is preserved
59
+ deleteOnExit?: boolean; // If true, clean up rules before process exit
60
+ protocol?: 'tcp' | 'udp' | 'all'; // Protocol to forward, defaults to 'tcp'
61
+ enableLogging?: boolean; // Enable detailed logging
62
+ ipv6Support?: boolean; // Enable IPv6 support
63
+ logFormat?: 'plain' | 'json'; // Format for logs
64
+
65
+ // Source filtering
66
+ allowedSourceIPs?: string[]; // If provided, only these IPs are allowed
67
+ bannedSourceIPs?: string[]; // If provided, these IPs are blocked
68
+ useIPSets?: boolean; // Use nftables sets for efficient IP management
69
+
70
+ // Rule management
71
+ forceCleanSlate?: boolean; // Clear all NfTablesProxy rules before starting
72
+ tableName?: string; // Custom table name (defaults to 'portproxy')
73
+
74
+ // Connection management
75
+ maxRetries?: number; // Maximum number of retries for failed commands
76
+ retryDelayMs?: number; // Delay between retries in milliseconds
77
+ useAdvancedNAT?: boolean; // Use connection tracking for stateful NAT
78
+
79
+ // Quality of Service
80
+ qos?: {
81
+ enabled: boolean;
82
+ maxRate?: string; // e.g. "10mbps"
83
+ priority?: number; // 1 (highest) to 10 (lowest)
84
+ markConnections?: boolean; // Mark connections for easier management
85
+ };
86
+
87
+ // Integration with PortProxy/NetworkProxy
88
+ netProxyIntegration?: {
89
+ enabled: boolean;
90
+ redirectLocalhost?: boolean; // Redirect localhost traffic to NetworkProxy
91
+ sslTerminationPort?: number; // Port where NetworkProxy handles SSL termination
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Represents a rule added to nftables
97
+ */
98
+ interface NfTablesRule {
99
+ handle?: number; // Rule handle for deletion
100
+ tableFamily: string; // 'ip' or 'ip6'
101
+ tableName: string; // Table name
102
+ chainName: string; // Chain name
103
+ ruleContents: string; // Rule definition
104
+ added: boolean; // Whether the rule was successfully added
105
+ verified?: boolean; // Whether the rule has been verified as applied
106
+ }
107
+
108
+ /**
109
+ * Interface for status reporting
110
+ */
111
+ export interface INfTablesStatus {
112
+ active: boolean;
113
+ ruleCount: {
114
+ total: number;
115
+ added: number;
116
+ verified: number;
117
+ };
118
+ tablesConfigured: { family: string; tableName: string }[];
119
+ metrics: {
120
+ forwardedConnections?: number;
121
+ activeConnections?: number;
122
+ bytesForwarded?: {
123
+ sent: number;
124
+ received: number;
125
+ };
126
+ };
127
+ qosEnabled?: boolean;
128
+ ipSetsConfigured?: {
129
+ name: string;
130
+ elementCount: number;
131
+ type: string;
132
+ }[];
133
+ }
134
+
135
+ /**
136
+ * NfTablesProxy sets up nftables NAT rules to forward TCP traffic.
137
+ * Enhanced with multi-port support, IPv6, connection tracking, metrics,
138
+ * and more advanced features.
139
+ */
140
+ export class NfTablesProxy {
141
+ public settings: INfTableProxySettings;
142
+ private rules: NfTablesRule[] = [];
143
+ private ipSets: Map<string, string[]> = new Map(); // Store IP sets for tracking
144
+ private ruleTag: string;
145
+ private tableName: string;
146
+ private tempFilePath: string;
147
+ private static NFT_CMD = 'nft';
148
+
149
+ constructor(settings: INfTableProxySettings) {
150
+ // Validate inputs to prevent command injection
151
+ this.validateSettings(settings);
152
+
153
+ // Set default settings
154
+ this.settings = {
155
+ ...settings,
156
+ toHost: settings.toHost || 'localhost',
157
+ protocol: settings.protocol || 'tcp',
158
+ enableLogging: settings.enableLogging !== undefined ? settings.enableLogging : false,
159
+ ipv6Support: settings.ipv6Support !== undefined ? settings.ipv6Support : false,
160
+ tableName: settings.tableName || 'portproxy',
161
+ logFormat: settings.logFormat || 'plain',
162
+ useIPSets: settings.useIPSets !== undefined ? settings.useIPSets : true,
163
+ maxRetries: settings.maxRetries || 3,
164
+ retryDelayMs: settings.retryDelayMs || 1000,
165
+ useAdvancedNAT: settings.useAdvancedNAT !== undefined ? settings.useAdvancedNAT : false,
166
+ };
167
+
168
+ // Generate a unique identifier for the rules added by this instance
169
+ this.ruleTag = `NfTablesProxy:${Date.now()}:${Math.random().toString(36).substr(2, 5)}`;
170
+
171
+ // Set table name
172
+ this.tableName = this.settings.tableName || 'portproxy';
173
+
174
+ // Create a temp file path for batch operations
175
+ this.tempFilePath = path.join(os.tmpdir(), `nft-rules-${Date.now()}.nft`);
176
+
177
+ // Register cleanup handlers if deleteOnExit is true
178
+ if (this.settings.deleteOnExit) {
179
+ const cleanup = () => {
180
+ try {
181
+ this.stopSync();
182
+ } catch (err) {
183
+ this.log('error', 'Error cleaning nftables rules on exit:', { error: err.message });
184
+ }
185
+ };
186
+
187
+ process.on('exit', cleanup);
188
+ process.on('SIGINT', () => {
189
+ cleanup();
190
+ process.exit();
191
+ });
192
+ process.on('SIGTERM', () => {
193
+ cleanup();
194
+ process.exit();
195
+ });
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Validates settings to prevent command injection and ensure valid values
201
+ */
202
+ private validateSettings(settings: INfTableProxySettings): void {
203
+ // Validate port numbers
204
+ const validatePorts = (port: number | IPortRange | Array<number | IPortRange>) => {
205
+ if (Array.isArray(port)) {
206
+ port.forEach(p => validatePorts(p));
207
+ return;
208
+ }
209
+
210
+ if (typeof port === 'number') {
211
+ if (port < 1 || port > 65535) {
212
+ throw new NftValidationError(`Invalid port number: ${port}`);
213
+ }
214
+ } else if (typeof port === 'object') {
215
+ if (port.from < 1 || port.from > 65535 || port.to < 1 || port.to > 65535 || port.from > port.to) {
216
+ throw new NftValidationError(`Invalid port range: ${port.from}-${port.to}`);
217
+ }
218
+ }
219
+ };
220
+
221
+ validatePorts(settings.fromPort);
222
+ validatePorts(settings.toPort);
223
+
224
+ // Define regex patterns for validation
225
+ const ipRegex = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
226
+ const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/;
227
+
228
+ // Validate IP addresses
229
+ const validateIPs = (ips?: string[]) => {
230
+ if (!ips) return;
231
+
232
+ for (const ip of ips) {
233
+ if (!ipRegex.test(ip) && !ipv6Regex.test(ip)) {
234
+ throw new NftValidationError(`Invalid IP address format: ${ip}`);
235
+ }
236
+ }
237
+ };
238
+
239
+ validateIPs(settings.allowedSourceIPs);
240
+ validateIPs(settings.bannedSourceIPs);
241
+
242
+ // Validate toHost - only allow hostnames or IPs
243
+ if (settings.toHost) {
244
+ const hostRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
245
+ if (!hostRegex.test(settings.toHost) && !ipRegex.test(settings.toHost) && !ipv6Regex.test(settings.toHost)) {
246
+ throw new NftValidationError(`Invalid host format: ${settings.toHost}`);
247
+ }
248
+ }
249
+
250
+ // Validate table name to prevent command injection
251
+ if (settings.tableName) {
252
+ const tableNameRegex = /^[a-zA-Z0-9_]+$/;
253
+ if (!tableNameRegex.test(settings.tableName)) {
254
+ throw new NftValidationError(`Invalid table name: ${settings.tableName}. Only alphanumeric characters and underscores are allowed.`);
255
+ }
256
+ }
257
+
258
+ // Validate QoS settings if enabled
259
+ if (settings.qos?.enabled) {
260
+ if (settings.qos.maxRate) {
261
+ const rateRegex = /^[0-9]+[kKmMgG]?bps$/;
262
+ if (!rateRegex.test(settings.qos.maxRate)) {
263
+ throw new NftValidationError(`Invalid rate format: ${settings.qos.maxRate}. Use format like "10mbps", "1gbps", etc.`);
264
+ }
265
+ }
266
+
267
+ if (settings.qos.priority !== undefined) {
268
+ if (settings.qos.priority < 1 || settings.qos.priority > 10 || !Number.isInteger(settings.qos.priority)) {
269
+ throw new NftValidationError(`Invalid priority: ${settings.qos.priority}. Must be an integer between 1 and 10.`);
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Normalizes port specifications into an array of port ranges
277
+ */
278
+ private normalizePortSpec(portSpec: number | IPortRange | Array<number | IPortRange>): IPortRange[] {
279
+ const result: IPortRange[] = [];
280
+
281
+ if (Array.isArray(portSpec)) {
282
+ // If it's an array, process each element
283
+ for (const spec of portSpec) {
284
+ result.push(...this.normalizePortSpec(spec));
285
+ }
286
+ } else if (typeof portSpec === 'number') {
287
+ // Single port becomes a range with the same start and end
288
+ result.push({ from: portSpec, to: portSpec });
289
+ } else {
290
+ // Already a range
291
+ result.push(portSpec);
292
+ }
293
+
294
+ return result;
295
+ }
296
+
297
+ /**
298
+ * Execute a command with retry capability
299
+ */
300
+ private async executeWithRetry(command: string, maxRetries = 3, retryDelayMs = 1000): Promise<string> {
301
+ let lastError: Error | undefined;
302
+
303
+ for (let i = 0; i < maxRetries; i++) {
304
+ try {
305
+ const { stdout } = await execAsync(command);
306
+ return stdout;
307
+ } catch (err) {
308
+ lastError = err;
309
+ this.log('warn', `Command failed (attempt ${i+1}/${maxRetries}): ${command}`, { error: err.message });
310
+
311
+ // Wait before retry, unless it's the last attempt
312
+ if (i < maxRetries - 1) {
313
+ await new Promise(resolve => setTimeout(resolve, retryDelayMs));
314
+ }
315
+ }
316
+ }
317
+
318
+ throw new NftExecutionError(`Failed after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
319
+ }
320
+
321
+ /**
322
+ * Execute system command synchronously with multiple attempts
323
+ */
324
+ private executeWithRetrySync(command: string, maxRetries = 3, retryDelayMs = 1000): string {
325
+ let lastError: Error | undefined;
326
+
327
+ for (let i = 0; i < maxRetries; i++) {
328
+ try {
329
+ return execSync(command).toString();
330
+ } catch (err) {
331
+ lastError = err;
332
+ this.log('warn', `Command failed (attempt ${i+1}/${maxRetries}): ${command}`, { error: err.message });
333
+
334
+ // Wait before retry, unless it's the last attempt
335
+ if (i < maxRetries - 1) {
336
+ // A naive sleep in sync context
337
+ const waitUntil = Date.now() + retryDelayMs;
338
+ while (Date.now() < waitUntil) {
339
+ // busy wait - not great, but this is a fallback method
340
+ }
341
+ }
342
+ }
343
+ }
344
+
345
+ throw new NftExecutionError(`Failed after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
346
+ }
347
+
348
+ /**
349
+ * Checks if nftables is available and the required modules are loaded
350
+ */
351
+ private async checkNftablesAvailability(): Promise<boolean> {
352
+ try {
353
+ await this.executeWithRetry(`${NfTablesProxy.NFT_CMD} --version`, this.settings.maxRetries, this.settings.retryDelayMs);
354
+
355
+ // Check for conntrack support if we're using advanced NAT
356
+ if (this.settings.useAdvancedNAT) {
357
+ try {
358
+ await this.executeWithRetry('lsmod | grep nf_conntrack', this.settings.maxRetries, this.settings.retryDelayMs);
359
+ } catch (err) {
360
+ this.log('warn', 'Connection tracking modules might not be loaded, advanced NAT features may not work');
361
+ }
362
+ }
363
+
364
+ return true;
365
+ } catch (err) {
366
+ this.log('error', `nftables is not available: ${err.message}`);
367
+ return false;
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Creates the necessary tables and chains
373
+ */
374
+ private async setupTablesAndChains(isIpv6: boolean = false): Promise<boolean> {
375
+ const family = isIpv6 ? 'ip6' : 'ip';
376
+
377
+ try {
378
+ // Check if the table already exists
379
+ const stdout = await this.executeWithRetry(
380
+ `${NfTablesProxy.NFT_CMD} list tables ${family}`,
381
+ this.settings.maxRetries,
382
+ this.settings.retryDelayMs
383
+ );
384
+
385
+ const tableExists = stdout.includes(`table ${family} ${this.tableName}`);
386
+
387
+ if (!tableExists) {
388
+ // Create the table
389
+ await this.executeWithRetry(
390
+ `${NfTablesProxy.NFT_CMD} add table ${family} ${this.tableName}`,
391
+ this.settings.maxRetries,
392
+ this.settings.retryDelayMs
393
+ );
394
+
395
+ this.log('info', `Created table ${family} ${this.tableName}`);
396
+
397
+ // Create the nat chain for the prerouting hook
398
+ await this.executeWithRetry(
399
+ `${NfTablesProxy.NFT_CMD} add chain ${family} ${this.tableName} nat_prerouting { type nat hook prerouting priority -100 ; }`,
400
+ this.settings.maxRetries,
401
+ this.settings.retryDelayMs
402
+ );
403
+
404
+ this.log('info', `Created nat_prerouting chain in ${family} ${this.tableName}`);
405
+
406
+ // Create the nat chain for the postrouting hook if not preserving source IP
407
+ if (!this.settings.preserveSourceIP) {
408
+ await this.executeWithRetry(
409
+ `${NfTablesProxy.NFT_CMD} add chain ${family} ${this.tableName} nat_postrouting { type nat hook postrouting priority 100 ; }`,
410
+ this.settings.maxRetries,
411
+ this.settings.retryDelayMs
412
+ );
413
+
414
+ this.log('info', `Created nat_postrouting chain in ${family} ${this.tableName}`);
415
+ }
416
+
417
+ // Create the chain for NetworkProxy integration if needed
418
+ if (this.settings.netProxyIntegration?.enabled && this.settings.netProxyIntegration.redirectLocalhost) {
419
+ await this.executeWithRetry(
420
+ `${NfTablesProxy.NFT_CMD} add chain ${family} ${this.tableName} nat_output { type nat hook output priority 0 ; }`,
421
+ this.settings.maxRetries,
422
+ this.settings.retryDelayMs
423
+ );
424
+
425
+ this.log('info', `Created nat_output chain in ${family} ${this.tableName}`);
426
+ }
427
+
428
+ // Create the QoS chain if needed
429
+ if (this.settings.qos?.enabled) {
430
+ await this.executeWithRetry(
431
+ `${NfTablesProxy.NFT_CMD} add chain ${family} ${this.tableName} qos_forward { type filter hook forward priority 0 ; }`,
432
+ this.settings.maxRetries,
433
+ this.settings.retryDelayMs
434
+ );
435
+
436
+ this.log('info', `Created QoS forward chain in ${family} ${this.tableName}`);
437
+ }
438
+ } else {
439
+ this.log('info', `Table ${family} ${this.tableName} already exists, using existing table`);
440
+ }
441
+
442
+ return true;
443
+ } catch (err) {
444
+ this.log('error', `Failed to set up tables and chains: ${err.message}`);
445
+ return false;
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Creates IP sets for efficient filtering of large IP lists
451
+ */
452
+ private async createIPSet(
453
+ family: string,
454
+ setName: string,
455
+ ips: string[],
456
+ setType: 'ipv4_addr' | 'ipv6_addr' = 'ipv4_addr'
457
+ ): Promise<boolean> {
458
+ try {
459
+ // Filter IPs based on family
460
+ const filteredIPs = ips.filter(ip => {
461
+ if (family === 'ip6' && ip.includes(':')) return true;
462
+ if (family === 'ip' && ip.includes('.')) return true;
463
+ return false;
464
+ });
465
+
466
+ if (filteredIPs.length === 0) {
467
+ this.log('info', `No IP addresses of type ${setType} to add to set ${setName}`);
468
+ return true;
469
+ }
470
+
471
+ // Check if set already exists
472
+ try {
473
+ const sets = await this.executeWithRetry(
474
+ `${NfTablesProxy.NFT_CMD} list sets ${family} ${this.tableName}`,
475
+ this.settings.maxRetries,
476
+ this.settings.retryDelayMs
477
+ );
478
+
479
+ if (sets.includes(`set ${setName} {`)) {
480
+ this.log('info', `IP set ${setName} already exists, will add elements`);
481
+ } else {
482
+ // Create the set
483
+ await this.executeWithRetry(
484
+ `${NfTablesProxy.NFT_CMD} add set ${family} ${this.tableName} ${setName} { type ${setType}; }`,
485
+ this.settings.maxRetries,
486
+ this.settings.retryDelayMs
487
+ );
488
+
489
+ this.log('info', `Created IP set ${setName} for ${family} with type ${setType}`);
490
+ }
491
+ } catch (err) {
492
+ // Set might not exist yet, create it
493
+ await this.executeWithRetry(
494
+ `${NfTablesProxy.NFT_CMD} add set ${family} ${this.tableName} ${setName} { type ${setType}; }`,
495
+ this.settings.maxRetries,
496
+ this.settings.retryDelayMs
497
+ );
498
+
499
+ this.log('info', `Created IP set ${setName} for ${family} with type ${setType}`);
500
+ }
501
+
502
+ // Add IPs to the set in batches to avoid command line length limitations
503
+ const batchSize = 100;
504
+ for (let i = 0; i < filteredIPs.length; i += batchSize) {
505
+ const batch = filteredIPs.slice(i, i + batchSize);
506
+ const elements = batch.join(', ');
507
+
508
+ await this.executeWithRetry(
509
+ `${NfTablesProxy.NFT_CMD} add element ${family} ${this.tableName} ${setName} { ${elements} }`,
510
+ this.settings.maxRetries,
511
+ this.settings.retryDelayMs
512
+ );
513
+
514
+ this.log('info', `Added batch of ${batch.length} IPs to set ${setName}`);
515
+ }
516
+
517
+ // Track the IP set
518
+ this.ipSets.set(`${family}:${setName}`, filteredIPs);
519
+
520
+ return true;
521
+ } catch (err) {
522
+ this.log('error', `Failed to create IP set ${setName}: ${err.message}`);
523
+ return false;
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Adds source IP filtering rules, potentially using IP sets for efficiency
529
+ */
530
+ private async addSourceIPFilters(isIpv6: boolean = false): Promise<boolean> {
531
+ if (!this.settings.allowedSourceIPs && !this.settings.bannedSourceIPs) {
532
+ return true; // Nothing to do
533
+ }
534
+
535
+ const family = isIpv6 ? 'ip6' : 'ip';
536
+ const chain = 'nat_prerouting';
537
+ const setType = isIpv6 ? 'ipv6_addr' : 'ipv4_addr';
538
+
539
+ try {
540
+ // Start building the ruleset file content
541
+ let rulesetContent = '';
542
+
543
+ // Using IP sets for more efficient rule processing with large IP lists
544
+ if (this.settings.useIPSets) {
545
+ // Create sets for banned and allowed IPs if needed
546
+ if (this.settings.bannedSourceIPs && this.settings.bannedSourceIPs.length > 0) {
547
+ const setName = 'banned_ips';
548
+ await this.createIPSet(family, setName, this.settings.bannedSourceIPs, setType as any);
549
+
550
+ // Add rule to drop traffic from banned IPs
551
+ const rule = `add rule ${family} ${this.tableName} ${chain} ip${isIpv6 ? '6' : ''} saddr @${setName} drop comment "${this.ruleTag}:BANNED_SET"`;
552
+ rulesetContent += `${rule}\n`;
553
+
554
+ this.rules.push({
555
+ tableFamily: family,
556
+ tableName: this.tableName,
557
+ chainName: chain,
558
+ ruleContents: rule,
559
+ added: false
560
+ });
561
+ }
562
+
563
+ if (this.settings.allowedSourceIPs && this.settings.allowedSourceIPs.length > 0) {
564
+ const setName = 'allowed_ips';
565
+ await this.createIPSet(family, setName, this.settings.allowedSourceIPs, setType as any);
566
+
567
+ // Add rule to allow traffic from allowed IPs
568
+ 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"`;
569
+ rulesetContent += `${rule}\n`;
570
+
571
+ this.rules.push({
572
+ tableFamily: family,
573
+ tableName: this.tableName,
574
+ chainName: chain,
575
+ ruleContents: rule,
576
+ added: false
577
+ });
578
+
579
+ // Add default deny rule for unlisted IPs
580
+ const denyRule = `add rule ${family} ${this.tableName} ${chain} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} drop comment "${this.ruleTag}:DENY_ALL"`;
581
+ rulesetContent += `${denyRule}\n`;
582
+
583
+ this.rules.push({
584
+ tableFamily: family,
585
+ tableName: this.tableName,
586
+ chainName: chain,
587
+ ruleContents: denyRule,
588
+ added: false
589
+ });
590
+ }
591
+ } else {
592
+ // Traditional approach without IP sets - less efficient for large IP lists
593
+
594
+ // Ban specific IPs first
595
+ if (this.settings.bannedSourceIPs && this.settings.bannedSourceIPs.length > 0) {
596
+ for (const ip of this.settings.bannedSourceIPs) {
597
+ // Skip IPv4 addresses for IPv6 rules and vice versa
598
+ if (isIpv6 && ip.includes('.')) continue;
599
+ if (!isIpv6 && ip.includes(':')) continue;
600
+
601
+ const rule = `add rule ${family} ${this.tableName} ${chain} ip${isIpv6 ? '6' : ''} saddr ${ip} drop comment "${this.ruleTag}:BANNED"`;
602
+ rulesetContent += `${rule}\n`;
603
+
604
+ this.rules.push({
605
+ tableFamily: family,
606
+ tableName: this.tableName,
607
+ chainName: chain,
608
+ ruleContents: rule,
609
+ added: false
610
+ });
611
+ }
612
+ }
613
+
614
+ // Allow specific IPs
615
+ if (this.settings.allowedSourceIPs && this.settings.allowedSourceIPs.length > 0) {
616
+ // Add rules to allow specific IPs
617
+ for (const ip of this.settings.allowedSourceIPs) {
618
+ // Skip IPv4 addresses for IPv6 rules and vice versa
619
+ if (isIpv6 && ip.includes('.')) continue;
620
+ if (!isIpv6 && ip.includes(':')) continue;
621
+
622
+ const rule = `add rule ${family} ${this.tableName} ${chain} ip${isIpv6 ? '6' : ''} saddr ${ip} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} accept comment "${this.ruleTag}:ALLOWED"`;
623
+ rulesetContent += `${rule}\n`;
624
+
625
+ this.rules.push({
626
+ tableFamily: family,
627
+ tableName: this.tableName,
628
+ chainName: chain,
629
+ ruleContents: rule,
630
+ added: false
631
+ });
632
+ }
633
+
634
+ // Add default deny rule for unlisted IPs
635
+ const denyRule = `add rule ${family} ${this.tableName} ${chain} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} drop comment "${this.ruleTag}:DENY_ALL"`;
636
+ rulesetContent += `${denyRule}\n`;
637
+
638
+ this.rules.push({
639
+ tableFamily: family,
640
+ tableName: this.tableName,
641
+ chainName: chain,
642
+ ruleContents: denyRule,
643
+ added: false
644
+ });
645
+ }
646
+ }
647
+
648
+ // Only write and apply if we have rules to add
649
+ if (rulesetContent) {
650
+ // Write the ruleset to a temporary file
651
+ fs.writeFileSync(this.tempFilePath, rulesetContent);
652
+
653
+ // Apply the ruleset
654
+ await this.executeWithRetry(
655
+ `${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
656
+ this.settings.maxRetries,
657
+ this.settings.retryDelayMs
658
+ );
659
+
660
+ this.log('info', `Added source IP filter rules for ${family}`);
661
+
662
+ // Mark rules as added
663
+ for (const rule of this.rules) {
664
+ if (rule.tableFamily === family && !rule.added) {
665
+ rule.added = true;
666
+
667
+ // Verify the rule was applied
668
+ await this.verifyRuleApplication(rule);
669
+ }
670
+ }
671
+
672
+ // Remove the temporary file
673
+ fs.unlinkSync(this.tempFilePath);
674
+ }
675
+
676
+ return true;
677
+ } catch (err) {
678
+ this.log('error', `Failed to add source IP filter rules: ${err.message}`);
679
+
680
+ // Try to clean up any rules that might have been added
681
+ this.rollbackRules();
682
+
683
+ return false;
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Gets a comma-separated list of all ports from a port specification
689
+ */
690
+ private getAllPorts(portSpec: number | IPortRange | Array<number | IPortRange>): string {
691
+ const portRanges = this.normalizePortSpec(portSpec);
692
+ const ports: string[] = [];
693
+
694
+ for (const range of portRanges) {
695
+ if (range.from === range.to) {
696
+ ports.push(range.from.toString());
697
+ } else {
698
+ ports.push(`${range.from}-${range.to}`);
699
+ }
700
+ }
701
+
702
+ return ports.join(', ');
703
+ }
704
+
705
+ /**
706
+ * Configures advanced NAT with connection tracking
707
+ */
708
+ private async setupAdvancedNAT(isIpv6: boolean = false): Promise<boolean> {
709
+ if (!this.settings.useAdvancedNAT) {
710
+ return true; // Skip if not using advanced NAT
711
+ }
712
+
713
+ const family = isIpv6 ? 'ip6' : 'ip';
714
+ const preroutingChain = 'nat_prerouting';
715
+
716
+ try {
717
+ // Get the port ranges
718
+ const fromPortRanges = this.normalizePortSpec(this.settings.fromPort);
719
+ const toPortRanges = this.normalizePortSpec(this.settings.toPort);
720
+
721
+ let rulesetContent = '';
722
+
723
+ // Simple case - one-to-one mapping with connection tracking
724
+ if (fromPortRanges.length === 1 && toPortRanges.length === 1) {
725
+ const fromRange = fromPortRanges[0];
726
+ const toRange = toPortRanges[0];
727
+
728
+ // Single port to single port with connection tracking
729
+ if (fromRange.from === fromRange.to && toRange.from === toRange.to) {
730
+ const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${fromRange.from} ct state new dnat to ${this.settings.toHost}:${toRange.from} comment "${this.ruleTag}:DNAT_CT"`;
731
+ rulesetContent += `${rule}\n`;
732
+
733
+ this.rules.push({
734
+ tableFamily: family,
735
+ tableName: this.tableName,
736
+ chainName: preroutingChain,
737
+ ruleContents: rule,
738
+ added: false
739
+ });
740
+ }
741
+ // Port range with same size
742
+ else if ((fromRange.to - fromRange.from) === (toRange.to - toRange.from)) {
743
+ const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${fromRange.from}-${fromRange.to} ct state new dnat to ${this.settings.toHost}:${toRange.from}-${toRange.to} comment "${this.ruleTag}:DNAT_RANGE_CT"`;
744
+ rulesetContent += `${rule}\n`;
745
+
746
+ this.rules.push({
747
+ tableFamily: family,
748
+ tableName: this.tableName,
749
+ chainName: preroutingChain,
750
+ ruleContents: rule,
751
+ added: false
752
+ });
753
+ }
754
+ // Add related and established connection rule for efficient connection handling
755
+ const ctRule = `add rule ${family} ${this.tableName} ${preroutingChain} ct state established,related accept comment "${this.ruleTag}:CT_ESTABLISHED"`;
756
+ rulesetContent += `${ctRule}\n`;
757
+
758
+ this.rules.push({
759
+ tableFamily: family,
760
+ tableName: this.tableName,
761
+ chainName: preroutingChain,
762
+ ruleContents: ctRule,
763
+ added: false
764
+ });
765
+
766
+ // Apply the rules if we have any
767
+ if (rulesetContent) {
768
+ fs.writeFileSync(this.tempFilePath, rulesetContent);
769
+
770
+ await this.executeWithRetry(
771
+ `${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
772
+ this.settings.maxRetries,
773
+ this.settings.retryDelayMs
774
+ );
775
+
776
+ this.log('info', `Added advanced NAT rules for ${family}`);
777
+
778
+ // Mark rules as added
779
+ for (const rule of this.rules) {
780
+ if (rule.tableFamily === family && !rule.added) {
781
+ rule.added = true;
782
+
783
+ // Verify the rule was applied
784
+ await this.verifyRuleApplication(rule);
785
+ }
786
+ }
787
+
788
+ // Remove the temporary file
789
+ fs.unlinkSync(this.tempFilePath);
790
+ }
791
+ }
792
+
793
+ return true;
794
+ } catch (err) {
795
+ this.log('error', `Failed to set up advanced NAT: ${err.message}`);
796
+ return false;
797
+ }
798
+ }
799
+
800
+ /**
801
+ * Adds port forwarding rules
802
+ */
803
+ private async addPortForwardingRules(isIpv6: boolean = false): Promise<boolean> {
804
+ // Skip if using advanced NAT as that already handles the port forwarding
805
+ if (this.settings.useAdvancedNAT) {
806
+ return true;
807
+ }
808
+
809
+ const family = isIpv6 ? 'ip6' : 'ip';
810
+ const preroutingChain = 'nat_prerouting';
811
+ const postroutingChain = 'nat_postrouting';
812
+
813
+ try {
814
+ // Normalize port specifications
815
+ const fromPortRanges = this.normalizePortSpec(this.settings.fromPort);
816
+ const toPortRanges = this.normalizePortSpec(this.settings.toPort);
817
+
818
+ // Handle the case where fromPort and toPort counts don't match
819
+ if (fromPortRanges.length !== toPortRanges.length) {
820
+ if (toPortRanges.length === 1) {
821
+ // If there's only one toPort, use it for all fromPorts
822
+ const singleToRange = toPortRanges[0];
823
+
824
+ return await this.addPortMappings(family, preroutingChain, postroutingChain, fromPortRanges, singleToRange);
825
+ } else {
826
+ throw new NftValidationError('Mismatched port counts: fromPort and toPort arrays must have equal length or toPort must be a single value');
827
+ }
828
+ } else {
829
+ // Add port mapping rules for each port pair
830
+ return await this.addPortPairMappings(family, preroutingChain, postroutingChain, fromPortRanges, toPortRanges);
831
+ }
832
+ } catch (err) {
833
+ this.log('error', `Failed to add port forwarding rules: ${err.message}`);
834
+ return false;
835
+ }
836
+ }
837
+
838
+ /**
839
+ * Adds port forwarding rules for the case where one toPortRange maps to multiple fromPortRanges
840
+ */
841
+ private async addPortMappings(
842
+ family: string,
843
+ preroutingChain: string,
844
+ postroutingChain: string,
845
+ fromPortRanges: IPortRange[],
846
+ toPortRange: IPortRange
847
+ ): Promise<boolean> {
848
+ try {
849
+ let rulesetContent = '';
850
+
851
+ // For each from port range, create a mapping to the single to port range
852
+ for (const fromRange of fromPortRanges) {
853
+ // Simple case: single port to single port
854
+ if (fromRange.from === fromRange.to && toPortRange.from === toPortRange.to) {
855
+ const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${fromRange.from} dnat to ${this.settings.toHost}:${toPortRange.from} comment "${this.ruleTag}:DNAT"`;
856
+ rulesetContent += `${rule}\n`;
857
+
858
+ this.rules.push({
859
+ tableFamily: family,
860
+ tableName: this.tableName,
861
+ chainName: preroutingChain,
862
+ ruleContents: rule,
863
+ added: false
864
+ });
865
+ }
866
+ // Multiple ports in from range, but only one port in to range
867
+ else if (toPortRange.from === toPortRange.to) {
868
+ // Map each port in from range to the single to port
869
+ for (let p = fromRange.from; p <= fromRange.to; p++) {
870
+ const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${p} dnat to ${this.settings.toHost}:${toPortRange.from} comment "${this.ruleTag}:DNAT"`;
871
+ rulesetContent += `${rule}\n`;
872
+
873
+ this.rules.push({
874
+ tableFamily: family,
875
+ tableName: this.tableName,
876
+ chainName: preroutingChain,
877
+ ruleContents: rule,
878
+ added: false
879
+ });
880
+ }
881
+ }
882
+ // Port range to port range mapping with modulo distribution
883
+ else {
884
+ const toRangeSize = toPortRange.to - toPortRange.from + 1;
885
+
886
+ for (let p = fromRange.from; p <= fromRange.to; p++) {
887
+ const offset = (p - fromRange.from) % toRangeSize;
888
+ const targetPort = toPortRange.from + offset;
889
+
890
+ const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${p} dnat to ${this.settings.toHost}:${targetPort} comment "${this.ruleTag}:DNAT"`;
891
+ rulesetContent += `${rule}\n`;
892
+
893
+ this.rules.push({
894
+ tableFamily: family,
895
+ tableName: this.tableName,
896
+ chainName: preroutingChain,
897
+ ruleContents: rule,
898
+ added: false
899
+ });
900
+ }
901
+ }
902
+ }
903
+
904
+ // Add masquerade rule for source NAT if not preserving source IP
905
+ if (!this.settings.preserveSourceIP) {
906
+ const ports = this.getAllPorts(this.settings.toPort);
907
+ const masqRule = `add rule ${family} ${this.tableName} ${postroutingChain} ${this.settings.protocol} daddr ${this.settings.toHost} dport {${ports}} masquerade comment "${this.ruleTag}:MASQ"`;
908
+ rulesetContent += `${masqRule}\n`;
909
+
910
+ this.rules.push({
911
+ tableFamily: family,
912
+ tableName: this.tableName,
913
+ chainName: postroutingChain,
914
+ ruleContents: masqRule,
915
+ added: false
916
+ });
917
+ }
918
+
919
+ // Apply the ruleset if we have any rules
920
+ if (rulesetContent) {
921
+ // Write to temporary file
922
+ fs.writeFileSync(this.tempFilePath, rulesetContent);
923
+
924
+ // Apply the ruleset
925
+ await this.executeWithRetry(
926
+ `${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
927
+ this.settings.maxRetries,
928
+ this.settings.retryDelayMs
929
+ );
930
+
931
+ this.log('info', `Added port forwarding rules for ${family}`);
932
+
933
+ // Mark rules as added
934
+ for (const rule of this.rules) {
935
+ if (rule.tableFamily === family && !rule.added) {
936
+ rule.added = true;
937
+
938
+ // Verify the rule was applied
939
+ await this.verifyRuleApplication(rule);
940
+ }
941
+ }
942
+
943
+ // Remove temporary file
944
+ fs.unlinkSync(this.tempFilePath);
945
+ }
946
+
947
+ return true;
948
+ } catch (err) {
949
+ this.log('error', `Failed to add port mappings: ${err.message}`);
950
+ return false;
951
+ }
952
+ }
953
+
954
+ /**
955
+ * Adds port forwarding rules for pairs of fromPortRanges and toPortRanges
956
+ */
957
+ private async addPortPairMappings(
958
+ family: string,
959
+ preroutingChain: string,
960
+ postroutingChain: string,
961
+ fromPortRanges: IPortRange[],
962
+ toPortRanges: IPortRange[]
963
+ ): Promise<boolean> {
964
+ try {
965
+ let rulesetContent = '';
966
+
967
+ // Process each fromPort and toPort pair
968
+ for (let i = 0; i < fromPortRanges.length; i++) {
969
+ const fromRange = fromPortRanges[i];
970
+ const toRange = toPortRanges[i];
971
+
972
+ // Simple case: single port to single port
973
+ if (fromRange.from === fromRange.to && toRange.from === toRange.to) {
974
+ const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${fromRange.from} dnat to ${this.settings.toHost}:${toRange.from} comment "${this.ruleTag}:DNAT"`;
975
+ rulesetContent += `${rule}\n`;
976
+
977
+ this.rules.push({
978
+ tableFamily: family,
979
+ tableName: this.tableName,
980
+ chainName: preroutingChain,
981
+ ruleContents: rule,
982
+ added: false
983
+ });
984
+ }
985
+ // Port range with equal size - can use direct mapping
986
+ else if ((fromRange.to - fromRange.from) === (toRange.to - toRange.from)) {
987
+ const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${fromRange.from}-${fromRange.to} dnat to ${this.settings.toHost}:${toRange.from}-${toRange.to} comment "${this.ruleTag}:DNAT_RANGE"`;
988
+ rulesetContent += `${rule}\n`;
989
+
990
+ this.rules.push({
991
+ tableFamily: family,
992
+ tableName: this.tableName,
993
+ chainName: preroutingChain,
994
+ ruleContents: rule,
995
+ added: false
996
+ });
997
+ }
998
+ // Unequal port ranges - need to map individually
999
+ else {
1000
+ const toRangeSize = toRange.to - toRange.from + 1;
1001
+
1002
+ for (let p = fromRange.from; p <= fromRange.to; p++) {
1003
+ const offset = (p - fromRange.from) % toRangeSize;
1004
+ const targetPort = toRange.from + offset;
1005
+
1006
+ const rule = `add rule ${family} ${this.tableName} ${preroutingChain} ${this.settings.protocol} dport ${p} dnat to ${this.settings.toHost}:${targetPort} comment "${this.ruleTag}:DNAT_INDIVIDUAL"`;
1007
+ rulesetContent += `${rule}\n`;
1008
+
1009
+ this.rules.push({
1010
+ tableFamily: family,
1011
+ tableName: this.tableName,
1012
+ chainName: preroutingChain,
1013
+ ruleContents: rule,
1014
+ added: false
1015
+ });
1016
+ }
1017
+ }
1018
+
1019
+ // Add masquerade rule for this port range if not preserving source IP
1020
+ if (!this.settings.preserveSourceIP) {
1021
+ const masqRule = `add rule ${family} ${this.tableName} ${postroutingChain} ${this.settings.protocol} daddr ${this.settings.toHost} dport ${toRange.from}-${toRange.to} masquerade comment "${this.ruleTag}:MASQ"`;
1022
+ rulesetContent += `${masqRule}\n`;
1023
+
1024
+ this.rules.push({
1025
+ tableFamily: family,
1026
+ tableName: this.tableName,
1027
+ chainName: postroutingChain,
1028
+ ruleContents: masqRule,
1029
+ added: false
1030
+ });
1031
+ }
1032
+ }
1033
+
1034
+ // Apply the ruleset if we have any rules
1035
+ if (rulesetContent) {
1036
+ // Write to temporary file
1037
+ fs.writeFileSync(this.tempFilePath, rulesetContent);
1038
+
1039
+ // Apply the ruleset
1040
+ await this.executeWithRetry(
1041
+ `${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
1042
+ this.settings.maxRetries,
1043
+ this.settings.retryDelayMs
1044
+ );
1045
+
1046
+ this.log('info', `Added port forwarding rules for ${family}`);
1047
+
1048
+ // Mark rules as added
1049
+ for (const rule of this.rules) {
1050
+ if (rule.tableFamily === family && !rule.added) {
1051
+ rule.added = true;
1052
+
1053
+ // Verify the rule was applied
1054
+ await this.verifyRuleApplication(rule);
1055
+ }
1056
+ }
1057
+
1058
+ // Remove temporary file
1059
+ fs.unlinkSync(this.tempFilePath);
1060
+ }
1061
+
1062
+ return true;
1063
+ } catch (err) {
1064
+ this.log('error', `Failed to add port pair mappings: ${err.message}`);
1065
+ return false;
1066
+ }
1067
+ }
1068
+
1069
+ /**
1070
+ * Setup quality of service rules
1071
+ */
1072
+ private async addTrafficShaping(isIpv6: boolean = false): Promise<boolean> {
1073
+ if (!this.settings.qos?.enabled) {
1074
+ return true;
1075
+ }
1076
+
1077
+ const family = isIpv6 ? 'ip6' : 'ip';
1078
+ const qosChain = 'qos_forward';
1079
+
1080
+ try {
1081
+ let rulesetContent = '';
1082
+
1083
+ // Add rate limiting rule if specified
1084
+ if (this.settings.qos.maxRate) {
1085
+ const ruleContent = `add rule ${family} ${this.tableName} ${qosChain} ip daddr ${this.settings.toHost} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.toPort)}} limit rate over ${this.settings.qos.maxRate} drop comment "${this.ruleTag}:QOS_RATE"`;
1086
+ rulesetContent += `${ruleContent}\n`;
1087
+
1088
+ this.rules.push({
1089
+ tableFamily: family,
1090
+ tableName: this.tableName,
1091
+ chainName: qosChain,
1092
+ ruleContents: ruleContent,
1093
+ added: false
1094
+ });
1095
+ }
1096
+
1097
+ // Add priority marking if specified
1098
+ if (this.settings.qos.priority !== undefined) {
1099
+ // Check if the chain exists
1100
+ const chainsOutput = await this.executeWithRetry(
1101
+ `${NfTablesProxy.NFT_CMD} list chains ${family} ${this.tableName}`,
1102
+ this.settings.maxRetries,
1103
+ this.settings.retryDelayMs
1104
+ );
1105
+
1106
+ // Check if we need to create priority queues
1107
+ const hasPrioChain = chainsOutput.includes(`chain prio${this.settings.qos.priority}`);
1108
+
1109
+ if (!hasPrioChain) {
1110
+ // Create priority chain
1111
+ const prioChainRule = `add chain ${family} ${this.tableName} prio${this.settings.qos.priority} { type filter hook forward priority ${this.settings.qos.priority * 10}; }`;
1112
+ rulesetContent += `${prioChainRule}\n`;
1113
+ }
1114
+
1115
+ // Add the rules to mark packets with this priority
1116
+ for (const range of this.normalizePortSpec(this.settings.toPort)) {
1117
+ const markRule = `add rule ${family} ${this.tableName} ${qosChain} ${this.settings.protocol} dport ${range.from}-${range.to} counter goto prio${this.settings.qos.priority} comment "${this.ruleTag}:QOS_PRIORITY"`;
1118
+ rulesetContent += `${markRule}\n`;
1119
+
1120
+ this.rules.push({
1121
+ tableFamily: family,
1122
+ tableName: this.tableName,
1123
+ chainName: qosChain,
1124
+ ruleContents: markRule,
1125
+ added: false
1126
+ });
1127
+ }
1128
+ }
1129
+
1130
+ // Apply the ruleset if we have any rules
1131
+ if (rulesetContent) {
1132
+ // Write to temporary file
1133
+ fs.writeFileSync(this.tempFilePath, rulesetContent);
1134
+
1135
+ // Apply the ruleset
1136
+ await this.executeWithRetry(
1137
+ `${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
1138
+ this.settings.maxRetries,
1139
+ this.settings.retryDelayMs
1140
+ );
1141
+
1142
+ this.log('info', `Added QoS rules for ${family}`);
1143
+
1144
+ // Mark rules as added
1145
+ for (const rule of this.rules) {
1146
+ if (rule.tableFamily === family && !rule.added) {
1147
+ rule.added = true;
1148
+
1149
+ // Verify the rule was applied
1150
+ await this.verifyRuleApplication(rule);
1151
+ }
1152
+ }
1153
+
1154
+ // Remove temporary file
1155
+ fs.unlinkSync(this.tempFilePath);
1156
+ }
1157
+
1158
+ return true;
1159
+ } catch (err) {
1160
+ this.log('error', `Failed to add traffic shaping: ${err.message}`);
1161
+ return false;
1162
+ }
1163
+ }
1164
+
1165
+ /**
1166
+ * Setup NetworkProxy integration rules
1167
+ */
1168
+ private async setupNetworkProxyIntegration(isIpv6: boolean = false): Promise<boolean> {
1169
+ if (!this.settings.netProxyIntegration?.enabled) {
1170
+ return true;
1171
+ }
1172
+
1173
+ const netProxyConfig = this.settings.netProxyIntegration;
1174
+ const family = isIpv6 ? 'ip6' : 'ip';
1175
+ const outputChain = 'nat_output';
1176
+
1177
+ try {
1178
+ // Only proceed if we're redirecting localhost and have a port
1179
+ if (netProxyConfig.redirectLocalhost && netProxyConfig.sslTerminationPort) {
1180
+ const localhost = isIpv6 ? '::1' : '127.0.0.1';
1181
+
1182
+ // Create the redirect rule
1183
+ const rule = `add rule ${family} ${this.tableName} ${outputChain} ${this.settings.protocol} daddr ${localhost} redirect to :${netProxyConfig.sslTerminationPort} comment "${this.ruleTag}:NETPROXY_REDIRECT"`;
1184
+
1185
+ // Apply the rule
1186
+ await this.executeWithRetry(
1187
+ `${NfTablesProxy.NFT_CMD} ${rule}`,
1188
+ this.settings.maxRetries,
1189
+ this.settings.retryDelayMs
1190
+ );
1191
+
1192
+ this.log('info', `Added NetworkProxy redirection rule for ${family}`);
1193
+
1194
+ const newRule = {
1195
+ tableFamily: family,
1196
+ tableName: this.tableName,
1197
+ chainName: outputChain,
1198
+ ruleContents: rule,
1199
+ added: true
1200
+ };
1201
+
1202
+ this.rules.push(newRule);
1203
+
1204
+ // Verify the rule was actually applied
1205
+ await this.verifyRuleApplication(newRule);
1206
+ }
1207
+
1208
+ return true;
1209
+ } catch (err) {
1210
+ this.log('error', `Failed to set up NetworkProxy integration: ${err.message}`);
1211
+ return false;
1212
+ }
1213
+ }
1214
+
1215
+ /**
1216
+ * Verify that a rule was successfully applied
1217
+ */
1218
+ private async verifyRuleApplication(rule: NfTablesRule): Promise<boolean> {
1219
+ try {
1220
+ const { tableFamily, tableName, chainName, ruleContents } = rule;
1221
+
1222
+ // Extract the distinctive parts of the rule to create a search pattern
1223
+ const commentMatch = ruleContents.match(/comment "([^"]+)"/);
1224
+ if (!commentMatch) return false;
1225
+
1226
+ const commentTag = commentMatch[1];
1227
+
1228
+ // List the chain to check if our rule is there
1229
+ const stdout = await this.executeWithRetry(
1230
+ `${NfTablesProxy.NFT_CMD} list chain ${tableFamily} ${tableName} ${chainName}`,
1231
+ this.settings.maxRetries,
1232
+ this.settings.retryDelayMs
1233
+ );
1234
+
1235
+ // Check if the comment appears in the output
1236
+ const isApplied = stdout.includes(commentTag);
1237
+
1238
+ rule.verified = isApplied;
1239
+
1240
+ if (!isApplied) {
1241
+ this.log('warn', `Rule verification failed: ${commentTag} not found in chain ${chainName}`);
1242
+ } else {
1243
+ this.log('debug', `Rule verified: ${commentTag} found in chain ${chainName}`);
1244
+ }
1245
+
1246
+ return isApplied;
1247
+ } catch (err) {
1248
+ this.log('error', `Failed to verify rule application: ${err.message}`);
1249
+ return false;
1250
+ }
1251
+ }
1252
+
1253
+ /**
1254
+ * Rolls back rules in case of error during setup
1255
+ */
1256
+ private async rollbackRules(): Promise<void> {
1257
+ // Process rules in reverse order (LIFO)
1258
+ for (let i = this.rules.length - 1; i >= 0; i--) {
1259
+ const rule = this.rules[i];
1260
+
1261
+ if (rule.added) {
1262
+ try {
1263
+ // For nftables, create a delete rule by replacing 'add' with 'delete'
1264
+ const deleteRule = rule.ruleContents.replace('add rule', 'delete rule');
1265
+ await this.executeWithRetry(
1266
+ `${NfTablesProxy.NFT_CMD} ${deleteRule}`,
1267
+ this.settings.maxRetries,
1268
+ this.settings.retryDelayMs
1269
+ );
1270
+
1271
+ this.log('info', `Rolled back rule: ${deleteRule}`);
1272
+
1273
+ rule.added = false;
1274
+ rule.verified = false;
1275
+ } catch (err) {
1276
+ this.log('error', `Failed to roll back rule: ${err.message}`);
1277
+ }
1278
+ }
1279
+ }
1280
+ }
1281
+
1282
+ /**
1283
+ * Checks if nftables table exists
1284
+ */
1285
+ private async tableExists(family: string, tableName: string): Promise<boolean> {
1286
+ try {
1287
+ const stdout = await this.executeWithRetry(
1288
+ `${NfTablesProxy.NFT_CMD} list tables ${family}`,
1289
+ this.settings.maxRetries,
1290
+ this.settings.retryDelayMs
1291
+ );
1292
+
1293
+ return stdout.includes(`table ${family} ${tableName}`);
1294
+ } catch (err) {
1295
+ return false;
1296
+ }
1297
+ }
1298
+
1299
+ /**
1300
+ * Get system metrics like connection counts
1301
+ */
1302
+ private async getSystemMetrics(): Promise<{
1303
+ activeConnections?: number;
1304
+ forwardedConnections?: number;
1305
+ bytesForwarded?: { sent: number; received: number };
1306
+ }> {
1307
+ const metrics: {
1308
+ activeConnections?: number;
1309
+ forwardedConnections?: number;
1310
+ bytesForwarded?: { sent: number; received: number };
1311
+ } = {};
1312
+
1313
+ try {
1314
+ // Try to get connection metrics if conntrack is available
1315
+ try {
1316
+ const stdout = await this.executeWithRetry('conntrack -C', this.settings.maxRetries, this.settings.retryDelayMs);
1317
+ metrics.activeConnections = parseInt(stdout.trim(), 10);
1318
+ } catch (err) {
1319
+ // conntrack not available, skip this metric
1320
+ }
1321
+
1322
+ // Try to get forwarded connections count from nftables counters
1323
+ try {
1324
+ // Look for counters in our rules
1325
+ const stdout = await this.executeWithRetry(
1326
+ `${NfTablesProxy.NFT_CMD} list table ip ${this.tableName}`,
1327
+ this.settings.maxRetries,
1328
+ this.settings.retryDelayMs
1329
+ );
1330
+
1331
+ // Parse counter information from the output
1332
+ const counterMatches = stdout.matchAll(/counter packets (\d+) bytes (\d+)/g);
1333
+ let totalPackets = 0;
1334
+ let totalBytes = 0;
1335
+
1336
+ for (const match of counterMatches) {
1337
+ totalPackets += parseInt(match[1], 10);
1338
+ totalBytes += parseInt(match[2], 10);
1339
+ }
1340
+
1341
+ if (totalPackets > 0) {
1342
+ metrics.forwardedConnections = totalPackets;
1343
+ metrics.bytesForwarded = {
1344
+ sent: totalBytes,
1345
+ received: 0 // We can't easily determine this without additional rules
1346
+ };
1347
+ }
1348
+ } catch (err) {
1349
+ // Failed to get counter info, skip this metric
1350
+ }
1351
+
1352
+ return metrics;
1353
+ } catch (err) {
1354
+ this.log('error', `Failed to get system metrics: ${err.message}`);
1355
+ return metrics;
1356
+ }
1357
+ }
1358
+
1359
+ /**
1360
+ * Get status of IP sets
1361
+ */
1362
+ private async getIPSetStatus(): Promise<{
1363
+ name: string;
1364
+ elementCount: number;
1365
+ type: string;
1366
+ }[]> {
1367
+ const result: {
1368
+ name: string;
1369
+ elementCount: number;
1370
+ type: string;
1371
+ }[] = [];
1372
+
1373
+ try {
1374
+ for (const family of ['ip', 'ip6']) {
1375
+ try {
1376
+ const stdout = await this.executeWithRetry(
1377
+ `${NfTablesProxy.NFT_CMD} list sets ${family} ${this.tableName}`,
1378
+ this.settings.maxRetries,
1379
+ this.settings.retryDelayMs
1380
+ );
1381
+
1382
+ const setMatches = stdout.matchAll(/set (\w+) {\s*type (\w+)/g);
1383
+
1384
+ for (const match of setMatches) {
1385
+ const setName = match[1];
1386
+ const setType = match[2];
1387
+
1388
+ // Get element count from tracking map
1389
+ const setKey = `${family}:${setName}`;
1390
+ const elements = this.ipSets.get(setKey) || [];
1391
+
1392
+ result.push({
1393
+ name: setName,
1394
+ elementCount: elements.length,
1395
+ type: setType
1396
+ });
1397
+ }
1398
+ } catch (err) {
1399
+ // No sets for this family, or table doesn't exist
1400
+ }
1401
+ }
1402
+
1403
+ return result;
1404
+ } catch (err) {
1405
+ this.log('error', `Failed to get IP set status: ${err.message}`);
1406
+ return result;
1407
+ }
1408
+ }
1409
+
1410
+ /**
1411
+ * Get detailed status about the current state of the proxy
1412
+ */
1413
+ public async getStatus(): Promise<INfTablesStatus> {
1414
+ const result: INfTablesStatus = {
1415
+ active: this.rules.some(r => r.added),
1416
+ ruleCount: {
1417
+ total: this.rules.length,
1418
+ added: this.rules.filter(r => r.added).length,
1419
+ verified: this.rules.filter(r => r.verified).length
1420
+ },
1421
+ tablesConfigured: [],
1422
+ metrics: {},
1423
+ qosEnabled: this.settings.qos?.enabled || false
1424
+ };
1425
+
1426
+ try {
1427
+ // Get list of configured tables
1428
+ const stdout = await this.executeWithRetry(
1429
+ `${NfTablesProxy.NFT_CMD} list tables`,
1430
+ this.settings.maxRetries,
1431
+ this.settings.retryDelayMs
1432
+ );
1433
+
1434
+ const tableRegex = /table (ip|ip6) (\w+)/g;
1435
+ let match;
1436
+
1437
+ while ((match = tableRegex.exec(stdout)) !== null) {
1438
+ const [, family, name] = match;
1439
+ if (name === this.tableName) {
1440
+ result.tablesConfigured.push({ family, tableName: name });
1441
+ }
1442
+ }
1443
+
1444
+ // Get system metrics
1445
+ result.metrics = await this.getSystemMetrics();
1446
+
1447
+ // Get IP set status if using IP sets
1448
+ if (this.settings.useIPSets) {
1449
+ result.ipSetsConfigured = await this.getIPSetStatus();
1450
+ }
1451
+
1452
+ return result;
1453
+ } catch (err) {
1454
+ this.log('error', `Failed to get status: ${err.message}`);
1455
+ return result;
1456
+ }
1457
+ }
1458
+
1459
+ /**
1460
+ * Performs a dry run to see what commands would be executed without actually applying them
1461
+ */
1462
+ public async dryRun(): Promise<string[]> {
1463
+ const commands: string[] = [];
1464
+
1465
+ // Simulate all the necessary setup steps and collect commands
1466
+
1467
+ // Tables and chains
1468
+ commands.push(`add table ip ${this.tableName}`);
1469
+ commands.push(`add chain ip ${this.tableName} nat_prerouting { type nat hook prerouting priority -100; }`);
1470
+
1471
+ if (!this.settings.preserveSourceIP) {
1472
+ commands.push(`add chain ip ${this.tableName} nat_postrouting { type nat hook postrouting priority 100; }`);
1473
+ }
1474
+
1475
+ if (this.settings.netProxyIntegration?.enabled && this.settings.netProxyIntegration.redirectLocalhost) {
1476
+ commands.push(`add chain ip ${this.tableName} nat_output { type nat hook output priority 0; }`);
1477
+ }
1478
+
1479
+ if (this.settings.qos?.enabled) {
1480
+ commands.push(`add chain ip ${this.tableName} qos_forward { type filter hook forward priority 0; }`);
1481
+ }
1482
+
1483
+ // Add IPv6 tables if enabled
1484
+ if (this.settings.ipv6Support) {
1485
+ commands.push(`add table ip6 ${this.tableName}`);
1486
+ commands.push(`add chain ip6 ${this.tableName} nat_prerouting { type nat hook prerouting priority -100; }`);
1487
+
1488
+ if (!this.settings.preserveSourceIP) {
1489
+ commands.push(`add chain ip6 ${this.tableName} nat_postrouting { type nat hook postrouting priority 100; }`);
1490
+ }
1491
+
1492
+ if (this.settings.netProxyIntegration?.enabled && this.settings.netProxyIntegration.redirectLocalhost) {
1493
+ commands.push(`add chain ip6 ${this.tableName} nat_output { type nat hook output priority 0; }`);
1494
+ }
1495
+
1496
+ if (this.settings.qos?.enabled) {
1497
+ commands.push(`add chain ip6 ${this.tableName} qos_forward { type filter hook forward priority 0; }`);
1498
+ }
1499
+ }
1500
+
1501
+ // Source IP filters
1502
+ if (this.settings.useIPSets) {
1503
+ if (this.settings.bannedSourceIPs?.length) {
1504
+ commands.push(`add set ip ${this.tableName} banned_ips { type ipv4_addr; }`);
1505
+ commands.push(`add element ip ${this.tableName} banned_ips { ${this.settings.bannedSourceIPs.join(', ')} }`);
1506
+ commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr @banned_ips drop comment "${this.ruleTag}:BANNED_SET"`);
1507
+ }
1508
+
1509
+ if (this.settings.allowedSourceIPs?.length) {
1510
+ commands.push(`add set ip ${this.tableName} allowed_ips { type ipv4_addr; }`);
1511
+ commands.push(`add element ip ${this.tableName} allowed_ips { ${this.settings.allowedSourceIPs.join(', ')} }`);
1512
+ 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"`);
1513
+ commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} drop comment "${this.ruleTag}:DENY_ALL"`);
1514
+ }
1515
+ } else if (this.settings.bannedSourceIPs?.length || this.settings.allowedSourceIPs?.length) {
1516
+ // Traditional approach without IP sets
1517
+ if (this.settings.bannedSourceIPs?.length) {
1518
+ for (const ip of this.settings.bannedSourceIPs) {
1519
+ commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr ${ip} drop comment "${this.ruleTag}:BANNED"`);
1520
+ }
1521
+ }
1522
+
1523
+ if (this.settings.allowedSourceIPs?.length) {
1524
+ for (const ip of this.settings.allowedSourceIPs) {
1525
+ 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"`);
1526
+ }
1527
+ commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} drop comment "${this.ruleTag}:DENY_ALL"`);
1528
+ }
1529
+ }
1530
+
1531
+ // Port forwarding rules
1532
+ if (this.settings.useAdvancedNAT) {
1533
+ // Advanced NAT with connection tracking
1534
+ const fromPortRanges = this.normalizePortSpec(this.settings.fromPort);
1535
+ const toPortRanges = this.normalizePortSpec(this.settings.toPort);
1536
+
1537
+ if (fromPortRanges.length === 1 && toPortRanges.length === 1) {
1538
+ const fromRange = fromPortRanges[0];
1539
+ const toRange = toPortRanges[0];
1540
+
1541
+ if (fromRange.from === fromRange.to && toRange.from === toRange.to) {
1542
+ commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport ${fromRange.from} ct state new dnat to ${this.settings.toHost}:${toRange.from} comment "${this.ruleTag}:DNAT_CT"`);
1543
+ } else if ((fromRange.to - fromRange.from) === (toRange.to - toRange.from)) {
1544
+ commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport ${fromRange.from}-${fromRange.to} ct state new dnat to ${this.settings.toHost}:${toRange.from}-${toRange.to} comment "${this.ruleTag}:DNAT_RANGE_CT"`);
1545
+ }
1546
+
1547
+ commands.push(`add rule ip ${this.tableName} nat_prerouting ct state established,related accept comment "${this.ruleTag}:CT_ESTABLISHED"`);
1548
+ }
1549
+ } else {
1550
+ // Standard NAT rules
1551
+ const fromRanges = this.normalizePortSpec(this.settings.fromPort);
1552
+ const toRanges = this.normalizePortSpec(this.settings.toPort);
1553
+
1554
+ if (fromRanges.length === 1 && toRanges.length === 1) {
1555
+ const fromRange = fromRanges[0];
1556
+ const toRange = toRanges[0];
1557
+
1558
+ if (fromRange.from === fromRange.to && toRange.from === toRange.to) {
1559
+ commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport ${fromRange.from} dnat to ${this.settings.toHost}:${toRange.from} comment "${this.ruleTag}:DNAT"`);
1560
+ } else {
1561
+ commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport ${fromRange.from}-${fromRange.to} dnat to ${this.settings.toHost}:${toRange.from}-${toRange.to} comment "${this.ruleTag}:DNAT_RANGE"`);
1562
+ }
1563
+ } else if (toRanges.length === 1) {
1564
+ // One-to-many mapping
1565
+ for (const fromRange of fromRanges) {
1566
+ commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport ${fromRange.from}-${fromRange.to} dnat to ${this.settings.toHost}:${toRanges[0].from}-${toRanges[0].to} comment "${this.ruleTag}:DNAT_RANGE"`);
1567
+ }
1568
+ } else {
1569
+ // One-to-one mapping of multiple ranges
1570
+ for (let i = 0; i < fromRanges.length; i++) {
1571
+ commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport ${fromRanges[i].from}-${fromRanges[i].to} dnat to ${this.settings.toHost}:${toRanges[i].from}-${toRanges[i].to} comment "${this.ruleTag}:DNAT_RANGE"`);
1572
+ }
1573
+ }
1574
+ }
1575
+
1576
+ // Masquerade rules if not preserving source IP
1577
+ if (!this.settings.preserveSourceIP) {
1578
+ commands.push(`add rule ip ${this.tableName} nat_postrouting ${this.settings.protocol} daddr ${this.settings.toHost} dport {${this.getAllPorts(this.settings.toPort)}} masquerade comment "${this.ruleTag}:MASQ"`);
1579
+ }
1580
+
1581
+ // NetworkProxy integration
1582
+ if (this.settings.netProxyIntegration?.enabled &&
1583
+ this.settings.netProxyIntegration.redirectLocalhost &&
1584
+ this.settings.netProxyIntegration.sslTerminationPort) {
1585
+
1586
+ commands.push(`add rule ip ${this.tableName} nat_output ${this.settings.protocol} daddr 127.0.0.1 redirect to :${this.settings.netProxyIntegration.sslTerminationPort} comment "${this.ruleTag}:NETPROXY_REDIRECT"`);
1587
+ }
1588
+
1589
+ // QoS rules
1590
+ if (this.settings.qos?.enabled) {
1591
+ if (this.settings.qos.maxRate) {
1592
+ commands.push(`add rule ip ${this.tableName} qos_forward ip daddr ${this.settings.toHost} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.toPort)}} limit rate over ${this.settings.qos.maxRate} drop comment "${this.ruleTag}:QOS_RATE"`);
1593
+ }
1594
+
1595
+ if (this.settings.qos.priority !== undefined) {
1596
+ commands.push(`add chain ip ${this.tableName} prio${this.settings.qos.priority} { type filter hook forward priority ${this.settings.qos.priority * 10}; }`);
1597
+
1598
+ for (const range of this.normalizePortSpec(this.settings.toPort)) {
1599
+ commands.push(`add rule ip ${this.tableName} qos_forward ${this.settings.protocol} dport ${range.from}-${range.to} counter goto prio${this.settings.qos.priority} comment "${this.ruleTag}:QOS_PRIORITY"`);
1600
+ }
1601
+ }
1602
+ }
1603
+
1604
+ return commands;
1605
+ }
1606
+
1607
+ /**
1608
+ * Starts the proxy by setting up all nftables rules
1609
+ */
1610
+ public async start(): Promise<void> {
1611
+ // Check if nftables is available
1612
+ const nftablesAvailable = await this.checkNftablesAvailability();
1613
+ if (!nftablesAvailable) {
1614
+ throw new NftResourceError('nftables is not available or not properly configured');
1615
+ }
1616
+
1617
+ // Optionally clean slate first
1618
+ if (this.settings.forceCleanSlate) {
1619
+ await NfTablesProxy.cleanSlate();
1620
+ }
1621
+
1622
+ // Set up tables and chains for IPv4
1623
+ const setupSuccess = await this.setupTablesAndChains();
1624
+ if (!setupSuccess) {
1625
+ throw new NftExecutionError('Failed to set up nftables tables and chains');
1626
+ }
1627
+
1628
+ // Set up IPv6 tables and chains if enabled
1629
+ if (this.settings.ipv6Support) {
1630
+ const setupIPv6Success = await this.setupTablesAndChains(true);
1631
+ if (!setupIPv6Success) {
1632
+ this.log('warn', 'Failed to set up IPv6 tables and chains, continuing with IPv4 only');
1633
+ }
1634
+ }
1635
+
1636
+ // Add source IP filters
1637
+ await this.addSourceIPFilters();
1638
+ if (this.settings.ipv6Support) {
1639
+ await this.addSourceIPFilters(true);
1640
+ }
1641
+
1642
+ // Set up advanced NAT with connection tracking if enabled
1643
+ if (this.settings.useAdvancedNAT) {
1644
+ const advancedNatSuccess = await this.setupAdvancedNAT();
1645
+ if (!advancedNatSuccess) {
1646
+ this.log('warn', 'Failed to set up advanced NAT, falling back to standard NAT');
1647
+ this.settings.useAdvancedNAT = false;
1648
+ } else if (this.settings.ipv6Support) {
1649
+ await this.setupAdvancedNAT(true);
1650
+ }
1651
+ }
1652
+
1653
+ // Add port forwarding rules (skip if using advanced NAT)
1654
+ if (!this.settings.useAdvancedNAT) {
1655
+ const forwardingSuccess = await this.addPortForwardingRules();
1656
+ if (!forwardingSuccess) {
1657
+ throw new NftExecutionError('Failed to add port forwarding rules');
1658
+ }
1659
+
1660
+ // Add IPv6 port forwarding rules if enabled
1661
+ if (this.settings.ipv6Support) {
1662
+ const forwardingIPv6Success = await this.addPortForwardingRules(true);
1663
+ if (!forwardingIPv6Success) {
1664
+ this.log('warn', 'Failed to add IPv6 port forwarding rules');
1665
+ }
1666
+ }
1667
+ }
1668
+
1669
+ // Set up QoS if enabled
1670
+ if (this.settings.qos?.enabled) {
1671
+ const qosSuccess = await this.addTrafficShaping();
1672
+ if (!qosSuccess) {
1673
+ this.log('warn', 'Failed to set up QoS rules, continuing without traffic shaping');
1674
+ } else if (this.settings.ipv6Support) {
1675
+ await this.addTrafficShaping(true);
1676
+ }
1677
+ }
1678
+
1679
+ // Set up NetworkProxy integration if enabled
1680
+ if (this.settings.netProxyIntegration?.enabled) {
1681
+ const netProxySetupSuccess = await this.setupNetworkProxyIntegration();
1682
+ if (!netProxySetupSuccess) {
1683
+ this.log('warn', 'Failed to set up NetworkProxy integration');
1684
+ }
1685
+
1686
+ if (this.settings.ipv6Support) {
1687
+ await this.setupNetworkProxyIntegration(true);
1688
+ }
1689
+ }
1690
+
1691
+ // Final check - ensure we have at least one rule added
1692
+ if (this.rules.filter(r => r.added).length === 0) {
1693
+ throw new NftExecutionError('No rules were added');
1694
+ }
1695
+
1696
+ this.log('info', 'NfTablesProxy started successfully');
1697
+ }
1698
+
1699
+ /**
1700
+ * Stops the proxy by removing all added rules
1701
+ */
1702
+ public async stop(): Promise<void> {
1703
+ try {
1704
+ let rulesetContent = '';
1705
+
1706
+ // Process rules in reverse order (LIFO)
1707
+ for (let i = this.rules.length - 1; i >= 0; i--) {
1708
+ const rule = this.rules[i];
1709
+
1710
+ if (rule.added) {
1711
+ // Create delete rules by replacing 'add' with 'delete'
1712
+ const deleteRule = rule.ruleContents.replace('add rule', 'delete rule');
1713
+ rulesetContent += `${deleteRule}\n`;
1714
+ }
1715
+ }
1716
+
1717
+ // Apply the ruleset if we have any rules to delete
1718
+ if (rulesetContent) {
1719
+ // Write to temporary file
1720
+ fs.writeFileSync(this.tempFilePath, rulesetContent);
1721
+
1722
+ // Apply the ruleset
1723
+ await this.executeWithRetry(
1724
+ `${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
1725
+ this.settings.maxRetries,
1726
+ this.settings.retryDelayMs
1727
+ );
1728
+
1729
+ this.log('info', 'Removed all added rules');
1730
+
1731
+ // Mark all rules as removed
1732
+ this.rules.forEach(rule => {
1733
+ rule.added = false;
1734
+ rule.verified = false;
1735
+ });
1736
+
1737
+ // Remove temporary file
1738
+ fs.unlinkSync(this.tempFilePath);
1739
+ }
1740
+
1741
+ // Clean up IP sets if we created any
1742
+ if (this.settings.useIPSets && this.ipSets.size > 0) {
1743
+ for (const [key, _] of this.ipSets) {
1744
+ const [family, setName] = key.split(':');
1745
+
1746
+ try {
1747
+ await this.executeWithRetry(
1748
+ `${NfTablesProxy.NFT_CMD} delete set ${family} ${this.tableName} ${setName}`,
1749
+ this.settings.maxRetries,
1750
+ this.settings.retryDelayMs
1751
+ );
1752
+
1753
+ this.log('info', `Removed IP set ${setName} from ${family} ${this.tableName}`);
1754
+ } catch (err) {
1755
+ this.log('warn', `Failed to remove IP set ${setName}: ${err.message}`);
1756
+ }
1757
+ }
1758
+
1759
+ this.ipSets.clear();
1760
+ }
1761
+
1762
+ // Optionally clean up tables if they're empty
1763
+ await this.cleanupEmptyTables();
1764
+
1765
+ this.log('info', 'NfTablesProxy stopped successfully');
1766
+ } catch (err) {
1767
+ this.log('error', `Error stopping NfTablesProxy: ${err.message}`);
1768
+ throw err;
1769
+ }
1770
+ }
1771
+
1772
+ /**
1773
+ * Synchronous version of stop, for use in exit handlers
1774
+ */
1775
+ public stopSync(): void {
1776
+ try {
1777
+ let rulesetContent = '';
1778
+
1779
+ // Process rules in reverse order (LIFO)
1780
+ for (let i = this.rules.length - 1; i >= 0; i--) {
1781
+ const rule = this.rules[i];
1782
+
1783
+ if (rule.added) {
1784
+ // Create delete rules by replacing 'add' with 'delete'
1785
+ const deleteRule = rule.ruleContents.replace('add rule', 'delete rule');
1786
+ rulesetContent += `${deleteRule}\n`;
1787
+ }
1788
+ }
1789
+
1790
+ // Apply the ruleset if we have any rules to delete
1791
+ if (rulesetContent) {
1792
+ // Write to temporary file
1793
+ fs.writeFileSync(this.tempFilePath, rulesetContent);
1794
+
1795
+ // Apply the ruleset
1796
+ this.executeWithRetrySync(
1797
+ `${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
1798
+ this.settings.maxRetries,
1799
+ this.settings.retryDelayMs
1800
+ );
1801
+
1802
+ this.log('info', 'Removed all added rules');
1803
+
1804
+ // Mark all rules as removed
1805
+ this.rules.forEach(rule => {
1806
+ rule.added = false;
1807
+ rule.verified = false;
1808
+ });
1809
+
1810
+ // Remove temporary file
1811
+ fs.unlinkSync(this.tempFilePath);
1812
+ }
1813
+
1814
+ // Clean up IP sets if we created any
1815
+ if (this.settings.useIPSets && this.ipSets.size > 0) {
1816
+ for (const [key, _] of this.ipSets) {
1817
+ const [family, setName] = key.split(':');
1818
+
1819
+ try {
1820
+ this.executeWithRetrySync(
1821
+ `${NfTablesProxy.NFT_CMD} delete set ${family} ${this.tableName} ${setName}`,
1822
+ this.settings.maxRetries,
1823
+ this.settings.retryDelayMs
1824
+ );
1825
+ } catch (err) {
1826
+ // Non-critical error, continue
1827
+ }
1828
+ }
1829
+ }
1830
+
1831
+ // Optionally clean up tables if they're empty (sync version)
1832
+ this.cleanupEmptyTablesSync();
1833
+
1834
+ this.log('info', 'NfTablesProxy stopped successfully');
1835
+ } catch (err) {
1836
+ this.log('error', `Error stopping NfTablesProxy: ${err.message}`);
1837
+ }
1838
+ }
1839
+
1840
+ /**
1841
+ * Cleans up empty tables
1842
+ */
1843
+ private async cleanupEmptyTables(): Promise<void> {
1844
+ // Check if tables are empty, and if so, delete them
1845
+ for (const family of ['ip', 'ip6']) {
1846
+ // Skip IPv6 if not enabled
1847
+ if (family === 'ip6' && !this.settings.ipv6Support) {
1848
+ continue;
1849
+ }
1850
+
1851
+ try {
1852
+ // Check if table exists
1853
+ const tableExists = await this.tableExists(family, this.tableName);
1854
+ if (!tableExists) {
1855
+ continue;
1856
+ }
1857
+
1858
+ // Check if the table has any rules
1859
+ const stdout = await this.executeWithRetry(
1860
+ `${NfTablesProxy.NFT_CMD} list table ${family} ${this.tableName}`,
1861
+ this.settings.maxRetries,
1862
+ this.settings.retryDelayMs
1863
+ );
1864
+
1865
+ const hasRules = stdout.includes('rule');
1866
+
1867
+ if (!hasRules) {
1868
+ // Table is empty, delete it
1869
+ await this.executeWithRetry(
1870
+ `${NfTablesProxy.NFT_CMD} delete table ${family} ${this.tableName}`,
1871
+ this.settings.maxRetries,
1872
+ this.settings.retryDelayMs
1873
+ );
1874
+
1875
+ this.log('info', `Deleted empty table ${family} ${this.tableName}`);
1876
+ }
1877
+ } catch (err) {
1878
+ this.log('error', `Error cleaning up tables: ${err.message}`);
1879
+ }
1880
+ }
1881
+ }
1882
+
1883
+ /**
1884
+ * Synchronous version of cleanupEmptyTables
1885
+ */
1886
+ private cleanupEmptyTablesSync(): void {
1887
+ // Check if tables are empty, and if so, delete them
1888
+ for (const family of ['ip', 'ip6']) {
1889
+ // Skip IPv6 if not enabled
1890
+ if (family === 'ip6' && !this.settings.ipv6Support) {
1891
+ continue;
1892
+ }
1893
+
1894
+ try {
1895
+ // Check if table exists
1896
+ const tableExistsOutput = this.executeWithRetrySync(
1897
+ `${NfTablesProxy.NFT_CMD} list tables ${family}`,
1898
+ this.settings.maxRetries,
1899
+ this.settings.retryDelayMs
1900
+ );
1901
+
1902
+ const tableExists = tableExistsOutput.includes(`table ${family} ${this.tableName}`);
1903
+
1904
+ if (!tableExists) {
1905
+ continue;
1906
+ }
1907
+
1908
+ // Check if the table has any rules
1909
+ const stdout = this.executeWithRetrySync(
1910
+ `${NfTablesProxy.NFT_CMD} list table ${family} ${this.tableName}`,
1911
+ this.settings.maxRetries,
1912
+ this.settings.retryDelayMs
1913
+ );
1914
+
1915
+ const hasRules = stdout.includes('rule');
1916
+
1917
+ if (!hasRules) {
1918
+ // Table is empty, delete it
1919
+ this.executeWithRetrySync(
1920
+ `${NfTablesProxy.NFT_CMD} delete table ${family} ${this.tableName}`,
1921
+ this.settings.maxRetries,
1922
+ this.settings.retryDelayMs
1923
+ );
1924
+
1925
+ this.log('info', `Deleted empty table ${family} ${this.tableName}`);
1926
+ }
1927
+ } catch (err) {
1928
+ this.log('error', `Error cleaning up tables: ${err.message}`);
1929
+ }
1930
+ }
1931
+ }
1932
+
1933
+ /**
1934
+ * Removes all nftables rules created by this module
1935
+ */
1936
+ public static async cleanSlate(): Promise<void> {
1937
+ try {
1938
+ // Check for rules with our comment pattern
1939
+ const stdout = await execAsync(`${NfTablesProxy.NFT_CMD} list ruleset`);
1940
+
1941
+ // Extract our tables
1942
+ const tableMatches = stdout.stdout.match(/table (ip|ip6) (\w+) {[^}]*NfTablesProxy:[^}]*}/g);
1943
+
1944
+ if (tableMatches) {
1945
+ for (const tableMatch of tableMatches) {
1946
+ // Extract table family and name
1947
+ const familyMatch = tableMatch.match(/table (ip|ip6) (\w+)/);
1948
+ if (familyMatch) {
1949
+ const family = familyMatch[1];
1950
+ const tableName = familyMatch[2];
1951
+
1952
+ // Delete the table
1953
+ await execAsync(`${NfTablesProxy.NFT_CMD} delete table ${family} ${tableName}`);
1954
+ console.log(`Deleted table ${family} ${tableName} containing NfTablesProxy rules`);
1955
+ }
1956
+ }
1957
+ } else {
1958
+ console.log('No NfTablesProxy rules found to clean up');
1959
+ }
1960
+ } catch (err) {
1961
+ console.error(`Error in cleanSlate: ${err}`);
1962
+ }
1963
+ }
1964
+
1965
+ /**
1966
+ * Synchronous version of cleanSlate
1967
+ */
1968
+ public static cleanSlateSync(): void {
1969
+ try {
1970
+ // Check for rules with our comment pattern
1971
+ const stdout = execSync(`${NfTablesProxy.NFT_CMD} list ruleset`).toString();
1972
+
1973
+ // Extract our tables
1974
+ const tableMatches = stdout.match(/table (ip|ip6) (\w+) {[^}]*NfTablesProxy:[^}]*}/g);
1975
+
1976
+ if (tableMatches) {
1977
+ for (const tableMatch of tableMatches) {
1978
+ // Extract table family and name
1979
+ const familyMatch = tableMatch.match(/table (ip|ip6) (\w+)/);
1980
+ if (familyMatch) {
1981
+ const family = familyMatch[1];
1982
+ const tableName = familyMatch[2];
1983
+
1984
+ // Delete the table
1985
+ execSync(`${NfTablesProxy.NFT_CMD} delete table ${family} ${tableName}`);
1986
+ console.log(`Deleted table ${family} ${tableName} containing NfTablesProxy rules`);
1987
+ }
1988
+ }
1989
+ } else {
1990
+ console.log('No NfTablesProxy rules found to clean up');
1991
+ }
1992
+ } catch (err) {
1993
+ console.error(`Error in cleanSlateSync: ${err}`);
1994
+ }
1995
+ }
1996
+
1997
+ /**
1998
+ * Improved logging with structured output
1999
+ */
2000
+ private log(level: 'info' | 'warn' | 'error' | 'debug', message: string, meta?: Record<string, any>): void {
2001
+ if (!this.settings.enableLogging && (level === 'info' || level === 'debug')) {
2002
+ return;
2003
+ }
2004
+
2005
+ const timestamp = new Date().toISOString();
2006
+
2007
+ const logData = {
2008
+ timestamp,
2009
+ level: level.toUpperCase(),
2010
+ message,
2011
+ ...meta,
2012
+ context: {
2013
+ instance: this.ruleTag,
2014
+ table: this.tableName
2015
+ }
2016
+ };
2017
+
2018
+ // Determine if output should be JSON or plain text based on settings
2019
+ const useJson = this.settings.logFormat === 'json';
2020
+
2021
+ if (useJson) {
2022
+ const logOutput = JSON.stringify(logData);
2023
+ console.log(logOutput);
2024
+ return;
2025
+ }
2026
+
2027
+ // Plain text format
2028
+ const metaStr = meta ? ` ${JSON.stringify(meta)}` : '';
2029
+
2030
+ switch (level) {
2031
+ case 'info':
2032
+ console.log(`[${timestamp}] [INFO] ${message}${metaStr}`);
2033
+ break;
2034
+ case 'warn':
2035
+ console.warn(`[${timestamp}] [WARN] ${message}${metaStr}`);
2036
+ break;
2037
+ case 'error':
2038
+ console.error(`[${timestamp}] [ERROR] ${message}${metaStr}`);
2039
+ break;
2040
+ case 'debug':
2041
+ console.log(`[${timestamp}] [DEBUG] ${message}${metaStr}`);
2042
+ break;
2043
+ }
2044
+ }
2045
+ }