@push.rocks/smartproxy 4.2.6 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,901 +0,0 @@
1
- import { exec, execSync } from 'child_process';
2
- import { promisify } from 'util';
3
-
4
- const execAsync = promisify(exec);
5
-
6
- /**
7
- * Represents a port range for forwarding
8
- */
9
- export interface IPortRange {
10
- from: number;
11
- to: number;
12
- }
13
-
14
- /**
15
- * Settings for IPTablesProxy.
16
- */
17
- export interface IIpTableProxySettings {
18
- // Basic settings
19
- fromPort: number | IPortRange | Array<number | IPortRange>; // Support single port, port range, or multiple ports/ranges
20
- toPort: number | IPortRange | Array<number | IPortRange>;
21
- toHost?: string; // Target host for proxying; defaults to 'localhost'
22
-
23
- // Advanced settings
24
- preserveSourceIP?: boolean; // If true, the original source IP is preserved
25
- deleteOnExit?: boolean; // If true, clean up marked iptables rules before process exit
26
- protocol?: 'tcp' | 'udp' | 'all'; // Protocol to forward, defaults to 'tcp'
27
- enableLogging?: boolean; // Enable detailed logging
28
- ipv6Support?: boolean; // Enable IPv6 support (ip6tables)
29
-
30
- // Source filtering
31
- allowedSourceIPs?: string[]; // If provided, only these IPs are allowed
32
- bannedSourceIPs?: string[]; // If provided, these IPs are blocked
33
-
34
- // Rule management
35
- forceCleanSlate?: boolean; // Clear all IPTablesProxy rules before starting
36
- addJumpRule?: boolean; // Add a custom chain for cleaner rule management
37
- checkExistingRules?: boolean; // Check if rules already exist before adding
38
-
39
- // Integration with PortProxy/NetworkProxy
40
- netProxyIntegration?: {
41
- enabled: boolean;
42
- redirectLocalhost?: boolean; // Redirect localhost traffic to NetworkProxy
43
- sslTerminationPort?: number; // Port where NetworkProxy handles SSL termination
44
- };
45
- }
46
-
47
- /**
48
- * Represents a rule added to iptables
49
- */
50
- interface IpTablesRule {
51
- table: string;
52
- chain: string;
53
- command: string;
54
- tag: string;
55
- added: boolean;
56
- }
57
-
58
- /**
59
- * IPTablesProxy sets up iptables NAT rules to forward TCP traffic.
60
- * Enhanced with multi-port support, IPv6, and integration with PortProxy/NetworkProxy.
61
- */
62
- export class IPTablesProxy {
63
- public settings: IIpTableProxySettings;
64
- private rules: IpTablesRule[] = [];
65
- private ruleTag: string;
66
- private customChain: string | null = null;
67
-
68
- constructor(settings: IIpTableProxySettings) {
69
- // Validate inputs to prevent command injection
70
- this.validateSettings(settings);
71
-
72
- // Set default settings
73
- this.settings = {
74
- ...settings,
75
- toHost: settings.toHost || 'localhost',
76
- protocol: settings.protocol || 'tcp',
77
- enableLogging: settings.enableLogging !== undefined ? settings.enableLogging : false,
78
- ipv6Support: settings.ipv6Support !== undefined ? settings.ipv6Support : false,
79
- checkExistingRules: settings.checkExistingRules !== undefined ? settings.checkExistingRules : true,
80
- netProxyIntegration: settings.netProxyIntegration || { enabled: false }
81
- };
82
-
83
- // Generate a unique identifier for the rules added by this instance
84
- this.ruleTag = `IPTablesProxy:${Date.now()}:${Math.random().toString(36).substr(2, 5)}`;
85
-
86
- if (this.settings.addJumpRule) {
87
- this.customChain = `IPTablesProxy_${Math.random().toString(36).substr(2, 5)}`;
88
- }
89
-
90
- // Register cleanup handlers if deleteOnExit is true
91
- if (this.settings.deleteOnExit) {
92
- const cleanup = () => {
93
- try {
94
- this.stopSync();
95
- } catch (err) {
96
- console.error('Error cleaning iptables rules on exit:', err);
97
- }
98
- };
99
-
100
- process.on('exit', cleanup);
101
- process.on('SIGINT', () => {
102
- cleanup();
103
- process.exit();
104
- });
105
- process.on('SIGTERM', () => {
106
- cleanup();
107
- process.exit();
108
- });
109
- }
110
- }
111
-
112
- /**
113
- * Validates settings to prevent command injection and ensure valid values
114
- */
115
- private validateSettings(settings: IIpTableProxySettings): void {
116
- // Validate port numbers
117
- const validatePorts = (port: number | IPortRange | Array<number | IPortRange>) => {
118
- if (Array.isArray(port)) {
119
- port.forEach(p => validatePorts(p));
120
- return;
121
- }
122
-
123
- if (typeof port === 'number') {
124
- if (port < 1 || port > 65535) {
125
- throw new Error(`Invalid port number: ${port}`);
126
- }
127
- } else if (typeof port === 'object') {
128
- if (port.from < 1 || port.from > 65535 || port.to < 1 || port.to > 65535 || port.from > port.to) {
129
- throw new Error(`Invalid port range: ${port.from}-${port.to}`);
130
- }
131
- }
132
- };
133
-
134
- validatePorts(settings.fromPort);
135
- validatePorts(settings.toPort);
136
-
137
- // Define regex patterns at the method level so they're available throughout
138
- 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]))?$/;
139
- 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]))?$/;
140
-
141
- // Validate IP addresses
142
- const validateIPs = (ips?: string[]) => {
143
- if (!ips) return;
144
-
145
- for (const ip of ips) {
146
- if (!ipRegex.test(ip) && !ipv6Regex.test(ip)) {
147
- throw new Error(`Invalid IP address format: ${ip}`);
148
- }
149
- }
150
- };
151
-
152
- validateIPs(settings.allowedSourceIPs);
153
- validateIPs(settings.bannedSourceIPs);
154
-
155
- // Validate toHost - only allow hostnames or IPs
156
- if (settings.toHost) {
157
- 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])$/;
158
- if (!hostRegex.test(settings.toHost) && !ipRegex.test(settings.toHost) && !ipv6Regex.test(settings.toHost)) {
159
- throw new Error(`Invalid host format: ${settings.toHost}`);
160
- }
161
- }
162
- }
163
-
164
- /**
165
- * Normalizes port specifications into an array of port ranges
166
- */
167
- private normalizePortSpec(portSpec: number | IPortRange | Array<number | IPortRange>): IPortRange[] {
168
- const result: IPortRange[] = [];
169
-
170
- if (Array.isArray(portSpec)) {
171
- // If it's an array, process each element
172
- for (const spec of portSpec) {
173
- result.push(...this.normalizePortSpec(spec));
174
- }
175
- } else if (typeof portSpec === 'number') {
176
- // Single port becomes a range with the same start and end
177
- result.push({ from: portSpec, to: portSpec });
178
- } else {
179
- // Already a range
180
- result.push(portSpec);
181
- }
182
-
183
- return result;
184
- }
185
-
186
- /**
187
- * Gets the appropriate iptables command based on settings
188
- */
189
- private getIptablesCommand(isIpv6: boolean = false): string {
190
- return isIpv6 ? 'ip6tables' : 'iptables';
191
- }
192
-
193
- /**
194
- * Checks if a rule already exists in iptables
195
- */
196
- private async ruleExists(table: string, command: string, isIpv6: boolean = false): Promise<boolean> {
197
- try {
198
- const iptablesCmd = this.getIptablesCommand(isIpv6);
199
- const { stdout } = await execAsync(`${iptablesCmd}-save -t ${table}`);
200
- // Convert the command to the format found in iptables-save output
201
- // (This is a simplification - in reality, you'd need more parsing)
202
- const rulePattern = command.replace(`${iptablesCmd} -t ${table} -A `, '-A ');
203
- return stdout.split('\n').some(line => line.trim() === rulePattern);
204
- } catch (err) {
205
- this.log('error', `Failed to check if rule exists: ${err}`);
206
- return false;
207
- }
208
- }
209
-
210
- /**
211
- * Sets up a custom chain for better rule management
212
- */
213
- private async setupCustomChain(isIpv6: boolean = false): Promise<boolean> {
214
- if (!this.customChain) return true;
215
-
216
- const iptablesCmd = this.getIptablesCommand(isIpv6);
217
- const table = 'nat';
218
-
219
- try {
220
- // Create the chain
221
- await execAsync(`${iptablesCmd} -t ${table} -N ${this.customChain}`);
222
- this.log('info', `Created custom chain: ${this.customChain}`);
223
-
224
- // Add jump rule to PREROUTING chain
225
- const jumpCommand = `${iptablesCmd} -t ${table} -A PREROUTING -j ${this.customChain} -m comment --comment "${this.ruleTag}:JUMP"`;
226
- await execAsync(jumpCommand);
227
- this.log('info', `Added jump rule to ${this.customChain}`);
228
-
229
- // Store the jump rule
230
- this.rules.push({
231
- table,
232
- chain: 'PREROUTING',
233
- command: jumpCommand,
234
- tag: `${this.ruleTag}:JUMP`,
235
- added: true
236
- });
237
-
238
- return true;
239
- } catch (err) {
240
- this.log('error', `Failed to set up custom chain: ${err}`);
241
- return false;
242
- }
243
- }
244
-
245
- /**
246
- * Add a source IP filter rule
247
- */
248
- private async addSourceIPFilter(isIpv6: boolean = false): Promise<boolean> {
249
- if (!this.settings.allowedSourceIPs && !this.settings.bannedSourceIPs) {
250
- return true;
251
- }
252
-
253
- const iptablesCmd = this.getIptablesCommand(isIpv6);
254
- const table = 'nat';
255
- const chain = this.customChain || 'PREROUTING';
256
-
257
- try {
258
- // Add banned IPs first (explicit deny)
259
- if (this.settings.bannedSourceIPs && this.settings.bannedSourceIPs.length > 0) {
260
- for (const ip of this.settings.bannedSourceIPs) {
261
- const command = `${iptablesCmd} -t ${table} -A ${chain} -s ${ip} -j DROP -m comment --comment "${this.ruleTag}:BANNED"`;
262
-
263
- // Check if rule already exists
264
- if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
265
- this.log('info', `Rule already exists, skipping: ${command}`);
266
- continue;
267
- }
268
-
269
- await execAsync(command);
270
- this.log('info', `Added banned IP rule: ${command}`);
271
-
272
- this.rules.push({
273
- table,
274
- chain,
275
- command,
276
- tag: `${this.ruleTag}:BANNED`,
277
- added: true
278
- });
279
- }
280
- }
281
-
282
- // Add allowed IPs (explicit allow)
283
- if (this.settings.allowedSourceIPs && this.settings.allowedSourceIPs.length > 0) {
284
- // First add a default deny for all
285
- const denyAllCommand = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} -j DROP -m comment --comment "${this.ruleTag}:DENY_ALL"`;
286
-
287
- // Add allow rules for specific IPs
288
- for (const ip of this.settings.allowedSourceIPs) {
289
- const command = `${iptablesCmd} -t ${table} -A ${chain} -s ${ip} -p ${this.settings.protocol} -j ACCEPT -m comment --comment "${this.ruleTag}:ALLOWED"`;
290
-
291
- // Check if rule already exists
292
- if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
293
- this.log('info', `Rule already exists, skipping: ${command}`);
294
- continue;
295
- }
296
-
297
- await execAsync(command);
298
- this.log('info', `Added allowed IP rule: ${command}`);
299
-
300
- this.rules.push({
301
- table,
302
- chain,
303
- command,
304
- tag: `${this.ruleTag}:ALLOWED`,
305
- added: true
306
- });
307
- }
308
-
309
- // Now add the default deny after all allows
310
- if (this.settings.checkExistingRules && await this.ruleExists(table, denyAllCommand, isIpv6)) {
311
- this.log('info', `Rule already exists, skipping: ${denyAllCommand}`);
312
- } else {
313
- await execAsync(denyAllCommand);
314
- this.log('info', `Added default deny rule: ${denyAllCommand}`);
315
-
316
- this.rules.push({
317
- table,
318
- chain,
319
- command: denyAllCommand,
320
- tag: `${this.ruleTag}:DENY_ALL`,
321
- added: true
322
- });
323
- }
324
- }
325
-
326
- return true;
327
- } catch (err) {
328
- this.log('error', `Failed to add source IP filter rules: ${err}`);
329
- return false;
330
- }
331
- }
332
-
333
- /**
334
- * Adds a port forwarding rule
335
- */
336
- private async addPortForwardingRule(
337
- fromPortRange: IPortRange,
338
- toPortRange: IPortRange,
339
- isIpv6: boolean = false
340
- ): Promise<boolean> {
341
- const iptablesCmd = this.getIptablesCommand(isIpv6);
342
- const table = 'nat';
343
- const chain = this.customChain || 'PREROUTING';
344
-
345
- try {
346
- // Handle single port case
347
- if (fromPortRange.from === fromPortRange.to && toPortRange.from === toPortRange.to) {
348
- // Single port forward
349
- const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPortRange.from} ` +
350
- `-j DNAT --to-destination ${this.settings.toHost}:${toPortRange.from} ` +
351
- `-m comment --comment "${this.ruleTag}:DNAT"`;
352
-
353
- // Check if rule already exists
354
- if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
355
- this.log('info', `Rule already exists, skipping: ${command}`);
356
- } else {
357
- await execAsync(command);
358
- this.log('info', `Added port forwarding rule: ${command}`);
359
-
360
- this.rules.push({
361
- table,
362
- chain,
363
- command,
364
- tag: `${this.ruleTag}:DNAT`,
365
- added: true
366
- });
367
- }
368
- } else if (fromPortRange.to - fromPortRange.from === toPortRange.to - toPortRange.from) {
369
- // Port range forward with equal ranges
370
- const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPortRange.from}:${fromPortRange.to} ` +
371
- `-j DNAT --to-destination ${this.settings.toHost}:${toPortRange.from}-${toPortRange.to} ` +
372
- `-m comment --comment "${this.ruleTag}:DNAT_RANGE"`;
373
-
374
- // Check if rule already exists
375
- if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
376
- this.log('info', `Rule already exists, skipping: ${command}`);
377
- } else {
378
- await execAsync(command);
379
- this.log('info', `Added port range forwarding rule: ${command}`);
380
-
381
- this.rules.push({
382
- table,
383
- chain,
384
- command,
385
- tag: `${this.ruleTag}:DNAT_RANGE`,
386
- added: true
387
- });
388
- }
389
- } else {
390
- // Unequal port ranges need individual rules
391
- for (let i = 0; i <= fromPortRange.to - fromPortRange.from; i++) {
392
- const fromPort = fromPortRange.from + i;
393
- const toPort = toPortRange.from + i % (toPortRange.to - toPortRange.from + 1);
394
-
395
- const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPort} ` +
396
- `-j DNAT --to-destination ${this.settings.toHost}:${toPort} ` +
397
- `-m comment --comment "${this.ruleTag}:DNAT_INDIVIDUAL"`;
398
-
399
- // Check if rule already exists
400
- if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
401
- this.log('info', `Rule already exists, skipping: ${command}`);
402
- continue;
403
- }
404
-
405
- await execAsync(command);
406
- this.log('info', `Added individual port forwarding rule: ${command}`);
407
-
408
- this.rules.push({
409
- table,
410
- chain,
411
- command,
412
- tag: `${this.ruleTag}:DNAT_INDIVIDUAL`,
413
- added: true
414
- });
415
- }
416
- }
417
-
418
- // If preserveSourceIP is false, add a MASQUERADE rule
419
- if (!this.settings.preserveSourceIP) {
420
- // For port range
421
- const masqCommand = `${iptablesCmd} -t nat -A POSTROUTING -p ${this.settings.protocol} -d ${this.settings.toHost} ` +
422
- `--dport ${toPortRange.from}:${toPortRange.to} -j MASQUERADE ` +
423
- `-m comment --comment "${this.ruleTag}:MASQ"`;
424
-
425
- // Check if rule already exists
426
- if (this.settings.checkExistingRules && await this.ruleExists('nat', masqCommand, isIpv6)) {
427
- this.log('info', `Rule already exists, skipping: ${masqCommand}`);
428
- } else {
429
- await execAsync(masqCommand);
430
- this.log('info', `Added MASQUERADE rule: ${masqCommand}`);
431
-
432
- this.rules.push({
433
- table: 'nat',
434
- chain: 'POSTROUTING',
435
- command: masqCommand,
436
- tag: `${this.ruleTag}:MASQ`,
437
- added: true
438
- });
439
- }
440
- }
441
-
442
- return true;
443
- } catch (err) {
444
- this.log('error', `Failed to add port forwarding rule: ${err}`);
445
-
446
- // Try to roll back any rules that were already added
447
- await this.rollbackRules();
448
-
449
- return false;
450
- }
451
- }
452
-
453
- /**
454
- * Special handling for NetworkProxy integration
455
- */
456
- private async setupNetworkProxyIntegration(isIpv6: boolean = false): Promise<boolean> {
457
- if (!this.settings.netProxyIntegration?.enabled) {
458
- return true;
459
- }
460
-
461
- const netProxyConfig = this.settings.netProxyIntegration;
462
- const iptablesCmd = this.getIptablesCommand(isIpv6);
463
- const table = 'nat';
464
- const chain = this.customChain || 'PREROUTING';
465
-
466
- try {
467
- // If redirectLocalhost is true, set up special rule to redirect localhost traffic to NetworkProxy
468
- if (netProxyConfig.redirectLocalhost && netProxyConfig.sslTerminationPort) {
469
- const redirectCommand = `${iptablesCmd} -t ${table} -A OUTPUT -p tcp -d 127.0.0.1 -j REDIRECT ` +
470
- `--to-port ${netProxyConfig.sslTerminationPort} ` +
471
- `-m comment --comment "${this.ruleTag}:NETPROXY_REDIRECT"`;
472
-
473
- // Check if rule already exists
474
- if (this.settings.checkExistingRules && await this.ruleExists(table, redirectCommand, isIpv6)) {
475
- this.log('info', `Rule already exists, skipping: ${redirectCommand}`);
476
- } else {
477
- await execAsync(redirectCommand);
478
- this.log('info', `Added NetworkProxy redirection rule: ${redirectCommand}`);
479
-
480
- this.rules.push({
481
- table,
482
- chain: 'OUTPUT',
483
- command: redirectCommand,
484
- tag: `${this.ruleTag}:NETPROXY_REDIRECT`,
485
- added: true
486
- });
487
- }
488
- }
489
-
490
- return true;
491
- } catch (err) {
492
- this.log('error', `Failed to set up NetworkProxy integration: ${err}`);
493
- return false;
494
- }
495
- }
496
-
497
- /**
498
- * Rolls back rules that were added in case of error
499
- */
500
- private async rollbackRules(): Promise<void> {
501
- // Process rules in reverse order (LIFO)
502
- for (let i = this.rules.length - 1; i >= 0; i--) {
503
- const rule = this.rules[i];
504
-
505
- if (rule.added) {
506
- try {
507
- // Convert -A (add) to -D (delete)
508
- const deleteCommand = rule.command.replace('-A', '-D');
509
- await execAsync(deleteCommand);
510
- this.log('info', `Rolled back rule: ${deleteCommand}`);
511
-
512
- rule.added = false;
513
- } catch (err) {
514
- this.log('error', `Failed to roll back rule: ${err}`);
515
- }
516
- }
517
- }
518
- }
519
-
520
- /**
521
- * Sets up iptables rules for port forwarding with enhanced features
522
- */
523
- public async start(): Promise<void> {
524
- // Optionally clean the slate first
525
- if (this.settings.forceCleanSlate) {
526
- await IPTablesProxy.cleanSlate();
527
- }
528
-
529
- // First set up any custom chains
530
- if (this.settings.addJumpRule) {
531
- const chainSetupSuccess = await this.setupCustomChain();
532
- if (!chainSetupSuccess) {
533
- throw new Error('Failed to set up custom chain');
534
- }
535
-
536
- // For IPv6 if enabled
537
- if (this.settings.ipv6Support) {
538
- const chainSetupSuccessIpv6 = await this.setupCustomChain(true);
539
- if (!chainSetupSuccessIpv6) {
540
- this.log('warn', 'Failed to set up IPv6 custom chain, continuing with IPv4 only');
541
- }
542
- }
543
- }
544
-
545
- // Add source IP filters
546
- await this.addSourceIPFilter();
547
- if (this.settings.ipv6Support) {
548
- await this.addSourceIPFilter(true);
549
- }
550
-
551
- // Set up NetworkProxy integration if enabled
552
- if (this.settings.netProxyIntegration?.enabled) {
553
- const netProxySetupSuccess = await this.setupNetworkProxyIntegration();
554
- if (!netProxySetupSuccess) {
555
- this.log('warn', 'Failed to set up NetworkProxy integration');
556
- }
557
-
558
- if (this.settings.ipv6Support) {
559
- await this.setupNetworkProxyIntegration(true);
560
- }
561
- }
562
-
563
- // Normalize port specifications
564
- const fromPortRanges = this.normalizePortSpec(this.settings.fromPort);
565
- const toPortRanges = this.normalizePortSpec(this.settings.toPort);
566
-
567
- // Handle the case where fromPort and toPort counts don't match
568
- if (fromPortRanges.length !== toPortRanges.length) {
569
- if (toPortRanges.length === 1) {
570
- // If there's only one toPort, use it for all fromPorts
571
- for (const fromRange of fromPortRanges) {
572
- await this.addPortForwardingRule(fromRange, toPortRanges[0]);
573
-
574
- if (this.settings.ipv6Support) {
575
- await this.addPortForwardingRule(fromRange, toPortRanges[0], true);
576
- }
577
- }
578
- } else {
579
- throw new Error('Mismatched port counts: fromPort and toPort arrays must have equal length or toPort must be a single value');
580
- }
581
- } else {
582
- // Add port forwarding rules for each port specification
583
- for (let i = 0; i < fromPortRanges.length; i++) {
584
- await this.addPortForwardingRule(fromPortRanges[i], toPortRanges[i]);
585
-
586
- if (this.settings.ipv6Support) {
587
- await this.addPortForwardingRule(fromPortRanges[i], toPortRanges[i], true);
588
- }
589
- }
590
- }
591
-
592
- // Final check - ensure we have at least one rule added
593
- if (this.rules.filter(r => r.added).length === 0) {
594
- throw new Error('No rules were added');
595
- }
596
- }
597
-
598
- /**
599
- * Removes all added iptables rules
600
- */
601
- public async stop(): Promise<void> {
602
- // Process rules in reverse order (LIFO)
603
- for (let i = this.rules.length - 1; i >= 0; i--) {
604
- const rule = this.rules[i];
605
-
606
- if (rule.added) {
607
- try {
608
- // Convert -A (add) to -D (delete)
609
- const deleteCommand = rule.command.replace('-A', '-D');
610
- await execAsync(deleteCommand);
611
- this.log('info', `Removed rule: ${deleteCommand}`);
612
-
613
- rule.added = false;
614
- } catch (err) {
615
- this.log('error', `Failed to remove rule: ${err}`);
616
- }
617
- }
618
- }
619
-
620
- // If we created a custom chain, we need to clean it up
621
- if (this.customChain) {
622
- try {
623
- // First flush the chain
624
- await execAsync(`iptables -t nat -F ${this.customChain}`);
625
- this.log('info', `Flushed custom chain: ${this.customChain}`);
626
-
627
- // Then delete it
628
- await execAsync(`iptables -t nat -X ${this.customChain}`);
629
- this.log('info', `Deleted custom chain: ${this.customChain}`);
630
-
631
- // Same for IPv6 if enabled
632
- if (this.settings.ipv6Support) {
633
- try {
634
- await execAsync(`ip6tables -t nat -F ${this.customChain}`);
635
- await execAsync(`ip6tables -t nat -X ${this.customChain}`);
636
- this.log('info', `Deleted IPv6 custom chain: ${this.customChain}`);
637
- } catch (err) {
638
- this.log('error', `Failed to delete IPv6 custom chain: ${err}`);
639
- }
640
- }
641
- } catch (err) {
642
- this.log('error', `Failed to delete custom chain: ${err}`);
643
- }
644
- }
645
-
646
- // Clear rules array
647
- this.rules = [];
648
- }
649
-
650
- /**
651
- * Synchronous version of stop, for use in exit handlers
652
- */
653
- public stopSync(): void {
654
- // Process rules in reverse order (LIFO)
655
- for (let i = this.rules.length - 1; i >= 0; i--) {
656
- const rule = this.rules[i];
657
-
658
- if (rule.added) {
659
- try {
660
- // Convert -A (add) to -D (delete)
661
- const deleteCommand = rule.command.replace('-A', '-D');
662
- execSync(deleteCommand);
663
- this.log('info', `Removed rule: ${deleteCommand}`);
664
-
665
- rule.added = false;
666
- } catch (err) {
667
- this.log('error', `Failed to remove rule: ${err}`);
668
- }
669
- }
670
- }
671
-
672
- // If we created a custom chain, we need to clean it up
673
- if (this.customChain) {
674
- try {
675
- // First flush the chain
676
- execSync(`iptables -t nat -F ${this.customChain}`);
677
-
678
- // Then delete it
679
- execSync(`iptables -t nat -X ${this.customChain}`);
680
- this.log('info', `Deleted custom chain: ${this.customChain}`);
681
-
682
- // Same for IPv6 if enabled
683
- if (this.settings.ipv6Support) {
684
- try {
685
- execSync(`ip6tables -t nat -F ${this.customChain}`);
686
- execSync(`ip6tables -t nat -X ${this.customChain}`);
687
- } catch (err) {
688
- // IPv6 failures are non-critical
689
- }
690
- }
691
- } catch (err) {
692
- this.log('error', `Failed to delete custom chain: ${err}`);
693
- }
694
- }
695
-
696
- // Clear rules array
697
- this.rules = [];
698
- }
699
-
700
- /**
701
- * Asynchronously cleans up any iptables rules in the nat table that were added by this module.
702
- * It looks for rules with comments containing "IPTablesProxy:".
703
- */
704
- public static async cleanSlate(): Promise<void> {
705
- await IPTablesProxy.cleanSlateInternal();
706
-
707
- // Also clean IPv6 rules
708
- await IPTablesProxy.cleanSlateInternal(true);
709
- }
710
-
711
- /**
712
- * Internal implementation of cleanSlate with IPv6 support
713
- */
714
- private static async cleanSlateInternal(isIpv6: boolean = false): Promise<void> {
715
- const iptablesCmd = isIpv6 ? 'ip6tables' : 'iptables';
716
-
717
- try {
718
- const { stdout } = await execAsync(`${iptablesCmd}-save -t nat`);
719
- const lines = stdout.split('\n');
720
- const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
721
-
722
- // First, find and remove any custom chains
723
- const customChains = new Set<string>();
724
- const jumpRules: string[] = [];
725
-
726
- for (const line of proxyLines) {
727
- if (line.includes('IPTablesProxy:JUMP')) {
728
- // Extract chain name from jump rule
729
- const match = line.match(/\s+-j\s+(\S+)\s+/);
730
- if (match && match[1].startsWith('IPTablesProxy_')) {
731
- customChains.add(match[1]);
732
- jumpRules.push(line);
733
- }
734
- }
735
- }
736
-
737
- // Remove jump rules first
738
- for (const line of jumpRules) {
739
- const trimmedLine = line.trim();
740
- if (trimmedLine.startsWith('-A')) {
741
- // Replace the "-A" with "-D" to form a deletion command
742
- const deleteRule = trimmedLine.replace('-A', '-D');
743
- const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
744
- try {
745
- await execAsync(cmd);
746
- console.log(`Cleaned up iptables jump rule: ${cmd}`);
747
- } catch (err) {
748
- console.error(`Failed to remove iptables jump rule: ${cmd}`, err);
749
- }
750
- }
751
- }
752
-
753
- // Then remove all other rules
754
- for (const line of proxyLines) {
755
- if (!line.includes('IPTablesProxy:JUMP')) { // Skip jump rules we already handled
756
- const trimmedLine = line.trim();
757
- if (trimmedLine.startsWith('-A')) {
758
- // Replace the "-A" with "-D" to form a deletion command
759
- const deleteRule = trimmedLine.replace('-A', '-D');
760
- const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
761
- try {
762
- await execAsync(cmd);
763
- console.log(`Cleaned up iptables rule: ${cmd}`);
764
- } catch (err) {
765
- console.error(`Failed to remove iptables rule: ${cmd}`, err);
766
- }
767
- }
768
- }
769
- }
770
-
771
- // Finally clean up custom chains
772
- for (const chain of customChains) {
773
- try {
774
- // Flush the chain
775
- await execAsync(`${iptablesCmd} -t nat -F ${chain}`);
776
- console.log(`Flushed custom chain: ${chain}`);
777
-
778
- // Delete the chain
779
- await execAsync(`${iptablesCmd} -t nat -X ${chain}`);
780
- console.log(`Deleted custom chain: ${chain}`);
781
- } catch (err) {
782
- console.error(`Failed to delete custom chain ${chain}:`, err);
783
- }
784
- }
785
- } catch (err) {
786
- console.error(`Failed to run ${iptablesCmd}-save: ${err}`);
787
- }
788
- }
789
-
790
- /**
791
- * Synchronously cleans up any iptables rules in the nat table that were added by this module.
792
- * It looks for rules with comments containing "IPTablesProxy:".
793
- * This method is intended for use in process exit handlers.
794
- */
795
- public static cleanSlateSync(): void {
796
- IPTablesProxy.cleanSlateSyncInternal();
797
-
798
- // Also clean IPv6 rules
799
- IPTablesProxy.cleanSlateSyncInternal(true);
800
- }
801
-
802
- /**
803
- * Internal implementation of cleanSlateSync with IPv6 support
804
- */
805
- private static cleanSlateSyncInternal(isIpv6: boolean = false): void {
806
- const iptablesCmd = isIpv6 ? 'ip6tables' : 'iptables';
807
-
808
- try {
809
- const stdout = execSync(`${iptablesCmd}-save -t nat`).toString();
810
- const lines = stdout.split('\n');
811
- const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
812
-
813
- // First, find and remove any custom chains
814
- const customChains = new Set<string>();
815
- const jumpRules: string[] = [];
816
-
817
- for (const line of proxyLines) {
818
- if (line.includes('IPTablesProxy:JUMP')) {
819
- // Extract chain name from jump rule
820
- const match = line.match(/\s+-j\s+(\S+)\s+/);
821
- if (match && match[1].startsWith('IPTablesProxy_')) {
822
- customChains.add(match[1]);
823
- jumpRules.push(line);
824
- }
825
- }
826
- }
827
-
828
- // Remove jump rules first
829
- for (const line of jumpRules) {
830
- const trimmedLine = line.trim();
831
- if (trimmedLine.startsWith('-A')) {
832
- // Replace the "-A" with "-D" to form a deletion command
833
- const deleteRule = trimmedLine.replace('-A', '-D');
834
- const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
835
- try {
836
- execSync(cmd);
837
- console.log(`Cleaned up iptables jump rule: ${cmd}`);
838
- } catch (err) {
839
- console.error(`Failed to remove iptables jump rule: ${cmd}`, err);
840
- }
841
- }
842
- }
843
-
844
- // Then remove all other rules
845
- for (const line of proxyLines) {
846
- if (!line.includes('IPTablesProxy:JUMP')) { // Skip jump rules we already handled
847
- const trimmedLine = line.trim();
848
- if (trimmedLine.startsWith('-A')) {
849
- const deleteRule = trimmedLine.replace('-A', '-D');
850
- const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
851
- try {
852
- execSync(cmd);
853
- console.log(`Cleaned up iptables rule: ${cmd}`);
854
- } catch (err) {
855
- console.error(`Failed to remove iptables rule: ${cmd}`, err);
856
- }
857
- }
858
- }
859
- }
860
-
861
- // Finally clean up custom chains
862
- for (const chain of customChains) {
863
- try {
864
- // Flush the chain
865
- execSync(`${iptablesCmd} -t nat -F ${chain}`);
866
-
867
- // Delete the chain
868
- execSync(`${iptablesCmd} -t nat -X ${chain}`);
869
- console.log(`Deleted custom chain: ${chain}`);
870
- } catch (err) {
871
- console.error(`Failed to delete custom chain ${chain}:`, err);
872
- }
873
- }
874
- } catch (err) {
875
- console.error(`Failed to run ${iptablesCmd}-save: ${err}`);
876
- }
877
- }
878
-
879
- /**
880
- * Logging utility that respects the enableLogging setting
881
- */
882
- private log(level: 'info' | 'warn' | 'error', message: string): void {
883
- if (!this.settings.enableLogging && level === 'info') {
884
- return;
885
- }
886
-
887
- const timestamp = new Date().toISOString();
888
-
889
- switch (level) {
890
- case 'info':
891
- console.log(`[${timestamp}] [INFO] ${message}`);
892
- break;
893
- case 'warn':
894
- console.warn(`[${timestamp}] [WARN] ${message}`);
895
- break;
896
- case 'error':
897
- console.error(`[${timestamp}] [ERROR] ${message}`);
898
- break;
899
- }
900
- }
901
- }