@push.rocks/smartproxy 3.28.6 → 3.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.iptablesproxy.d.ts +79 -7
- package/dist_ts/classes.iptablesproxy.js +662 -67
- package/dist_ts/classes.networkproxy.d.ts +46 -1
- package/dist_ts/classes.networkproxy.js +347 -8
- package/dist_ts/classes.portproxy.d.ts +36 -0
- package/dist_ts/classes.portproxy.js +464 -365
- package/package.json +2 -2
- package/readme.md +80 -10
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.iptablesproxy.ts +786 -68
- package/ts/classes.networkproxy.ts +417 -7
- package/ts/classes.portproxy.ts +652 -485
|
@@ -3,43 +3,100 @@ import { promisify } from 'util';
|
|
|
3
3
|
|
|
4
4
|
const execAsync = promisify(exec);
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Represents a port range for forwarding
|
|
8
|
+
*/
|
|
9
|
+
export interface IPortRange {
|
|
10
|
+
from: number;
|
|
11
|
+
to: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
/**
|
|
7
15
|
* Settings for IPTablesProxy.
|
|
8
16
|
*/
|
|
9
17
|
export interface IIpTableProxySettings {
|
|
10
|
-
|
|
11
|
-
|
|
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>;
|
|
12
21
|
toHost?: string; // Target host for proxying; defaults to 'localhost'
|
|
13
|
-
|
|
14
|
-
|
|
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;
|
|
15
56
|
}
|
|
16
57
|
|
|
17
58
|
/**
|
|
18
59
|
* IPTablesProxy sets up iptables NAT rules to forward TCP traffic.
|
|
19
|
-
*
|
|
60
|
+
* Enhanced with multi-port support, IPv6, and integration with PortProxy/NetworkProxy.
|
|
20
61
|
*/
|
|
21
62
|
export class IPTablesProxy {
|
|
22
63
|
public settings: IIpTableProxySettings;
|
|
23
|
-
private
|
|
64
|
+
private rules: IpTablesRule[] = [];
|
|
24
65
|
private ruleTag: string;
|
|
66
|
+
private customChain: string | null = null;
|
|
25
67
|
|
|
26
68
|
constructor(settings: IIpTableProxySettings) {
|
|
69
|
+
// Validate inputs to prevent command injection
|
|
70
|
+
this.validateSettings(settings);
|
|
71
|
+
|
|
72
|
+
// Set default settings
|
|
27
73
|
this.settings = {
|
|
28
74
|
...settings,
|
|
29
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 }
|
|
30
81
|
};
|
|
31
|
-
|
|
82
|
+
|
|
83
|
+
// Generate a unique identifier for the rules added by this instance
|
|
32
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
|
+
}
|
|
33
89
|
|
|
34
|
-
//
|
|
90
|
+
// Register cleanup handlers if deleteOnExit is true
|
|
35
91
|
if (this.settings.deleteOnExit) {
|
|
36
92
|
const cleanup = () => {
|
|
37
93
|
try {
|
|
38
|
-
|
|
94
|
+
this.stopSync();
|
|
39
95
|
} catch (err) {
|
|
40
96
|
console.error('Error cleaning iptables rules on exit:', err);
|
|
41
97
|
}
|
|
42
98
|
};
|
|
99
|
+
|
|
43
100
|
process.on('exit', cleanup);
|
|
44
101
|
process.on('SIGINT', () => {
|
|
45
102
|
cleanup();
|
|
@@ -53,76 +110,591 @@ export class IPTablesProxy {
|
|
|
53
110
|
}
|
|
54
111
|
|
|
55
112
|
/**
|
|
56
|
-
*
|
|
57
|
-
* The rules are tagged with a unique comment so that they can be identified later.
|
|
113
|
+
* Validates settings to prevent command injection and ensure valid values
|
|
58
114
|
*/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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> {
|
|
63
197
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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);
|
|
67
204
|
} catch (err) {
|
|
68
|
-
|
|
69
|
-
|
|
205
|
+
this.log('error', `Failed to check if rule exists: ${err}`);
|
|
206
|
+
return false;
|
|
70
207
|
}
|
|
208
|
+
}
|
|
71
209
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
});
|
|
91
279
|
}
|
|
92
|
-
throw err;
|
|
93
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;
|
|
94
330
|
}
|
|
95
331
|
}
|
|
96
332
|
|
|
97
333
|
/**
|
|
98
|
-
*
|
|
334
|
+
* Adds a port forwarding rule
|
|
99
335
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
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
|
+
}
|
|
102
452
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
|
|
106
466
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
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;
|
|
109
491
|
} catch (err) {
|
|
110
|
-
|
|
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();
|
|
111
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
|
+
}
|
|
112
597
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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) {
|
|
117
622
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
}
|
|
120
641
|
} catch (err) {
|
|
121
|
-
|
|
642
|
+
this.log('error', `Failed to delete custom chain: ${err}`);
|
|
122
643
|
}
|
|
123
644
|
}
|
|
645
|
+
|
|
646
|
+
// Clear rules array
|
|
647
|
+
this.rules = [];
|
|
648
|
+
}
|
|
124
649
|
|
|
125
|
-
|
|
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 = [];
|
|
126
698
|
}
|
|
127
699
|
|
|
128
700
|
/**
|
|
@@ -130,26 +702,88 @@ export class IPTablesProxy {
|
|
|
130
702
|
* It looks for rules with comments containing "IPTablesProxy:".
|
|
131
703
|
*/
|
|
132
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
|
+
|
|
133
717
|
try {
|
|
134
|
-
const { stdout } = await execAsync(
|
|
718
|
+
const { stdout } = await execAsync(`${iptablesCmd}-save -t nat`);
|
|
135
719
|
const lines = stdout.split('\n');
|
|
136
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
|
+
|
|
137
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) {
|
|
138
739
|
const trimmedLine = line.trim();
|
|
139
740
|
if (trimmedLine.startsWith('-A')) {
|
|
140
|
-
// Replace the "-A" with "-D" to form a deletion command
|
|
741
|
+
// Replace the "-A" with "-D" to form a deletion command
|
|
141
742
|
const deleteRule = trimmedLine.replace('-A', '-D');
|
|
142
|
-
const cmd =
|
|
743
|
+
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
|
|
143
744
|
try {
|
|
144
745
|
await execAsync(cmd);
|
|
145
|
-
console.log(`Cleaned up iptables rule: ${cmd}`);
|
|
746
|
+
console.log(`Cleaned up iptables jump rule: ${cmd}`);
|
|
146
747
|
} catch (err) {
|
|
147
|
-
console.error(`Failed to remove iptables rule: ${cmd}`, 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
|
+
}
|
|
148
767
|
}
|
|
149
768
|
}
|
|
150
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
|
+
}
|
|
151
785
|
} catch (err) {
|
|
152
|
-
console.error(`Failed to run
|
|
786
|
+
console.error(`Failed to run ${iptablesCmd}-save: ${err}`);
|
|
153
787
|
}
|
|
154
788
|
}
|
|
155
789
|
|
|
@@ -159,25 +793,109 @@ export class IPTablesProxy {
|
|
|
159
793
|
* This method is intended for use in process exit handlers.
|
|
160
794
|
*/
|
|
161
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
|
+
|
|
162
808
|
try {
|
|
163
|
-
const stdout = execSync(
|
|
809
|
+
const stdout = execSync(`${iptablesCmd}-save -t nat`).toString();
|
|
164
810
|
const lines = stdout.split('\n');
|
|
165
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
|
+
|
|
166
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) {
|
|
167
830
|
const trimmedLine = line.trim();
|
|
168
831
|
if (trimmedLine.startsWith('-A')) {
|
|
832
|
+
// Replace the "-A" with "-D" to form a deletion command
|
|
169
833
|
const deleteRule = trimmedLine.replace('-A', '-D');
|
|
170
|
-
const cmd =
|
|
834
|
+
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
|
|
171
835
|
try {
|
|
172
836
|
execSync(cmd);
|
|
173
|
-
console.log(`Cleaned up iptables rule: ${cmd}`);
|
|
837
|
+
console.log(`Cleaned up iptables jump rule: ${cmd}`);
|
|
174
838
|
} catch (err) {
|
|
175
|
-
console.error(`Failed to remove iptables rule: ${cmd}`, 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
|
+
}
|
|
176
857
|
}
|
|
177
858
|
}
|
|
178
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
|
+
}
|
|
179
874
|
} catch (err) {
|
|
180
|
-
console.error(`Failed to run
|
|
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;
|
|
181
899
|
}
|
|
182
900
|
}
|
|
183
901
|
}
|