@push.rocks/smartproxy 21.1.6 → 22.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/changelog.md +89 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/core/utils/shared-security-manager.d.ts +17 -0
  4. package/dist_ts/core/utils/shared-security-manager.js +66 -1
  5. package/dist_ts/proxies/http-proxy/default-certificates.d.ts +54 -0
  6. package/dist_ts/proxies/http-proxy/default-certificates.js +127 -0
  7. package/dist_ts/proxies/http-proxy/http-proxy.d.ts +1 -1
  8. package/dist_ts/proxies/http-proxy/http-proxy.js +9 -14
  9. package/dist_ts/proxies/http-proxy/index.d.ts +5 -1
  10. package/dist_ts/proxies/http-proxy/index.js +6 -2
  11. package/dist_ts/proxies/http-proxy/security-manager.d.ts +4 -12
  12. package/dist_ts/proxies/http-proxy/security-manager.js +66 -99
  13. package/dist_ts/proxies/nftables-proxy/index.d.ts +1 -0
  14. package/dist_ts/proxies/nftables-proxy/index.js +2 -1
  15. package/dist_ts/proxies/nftables-proxy/nftables-proxy.d.ts +4 -26
  16. package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +84 -236
  17. package/dist_ts/proxies/nftables-proxy/utils/index.d.ts +9 -0
  18. package/dist_ts/proxies/nftables-proxy/utils/index.js +12 -0
  19. package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.d.ts +66 -0
  20. package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.js +131 -0
  21. package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.d.ts +39 -0
  22. package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.js +112 -0
  23. package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.d.ts +59 -0
  24. package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.js +130 -0
  25. package/dist_ts/proxies/smart-proxy/certificate-manager.js +4 -3
  26. package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +13 -2
  27. package/dist_ts/proxies/smart-proxy/connection-manager.js +16 -6
  28. package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +35 -10
  29. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +0 -1
  30. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +17 -0
  31. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +72 -9
  32. package/dist_ts/proxies/smart-proxy/security-manager.d.ts +14 -12
  33. package/dist_ts/proxies/smart-proxy/security-manager.js +80 -74
  34. package/dist_ts/proxies/smart-proxy/smart-proxy.js +1 -2
  35. package/dist_ts/proxies/smart-proxy/tls-manager.d.ts +2 -9
  36. package/dist_ts/proxies/smart-proxy/tls-manager.js +3 -26
  37. package/dist_ts/proxies/smart-proxy/utils/index.d.ts +1 -1
  38. package/dist_ts/proxies/smart-proxy/utils/index.js +3 -4
  39. package/dist_ts/proxies/smart-proxy/utils/route-helpers/api-helpers.d.ts +49 -0
  40. package/dist_ts/proxies/smart-proxy/utils/route-helpers/api-helpers.js +108 -0
  41. package/dist_ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.d.ts +57 -0
  42. package/dist_ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.js +89 -0
  43. package/dist_ts/proxies/smart-proxy/utils/route-helpers/http-helpers.d.ts +17 -0
  44. package/dist_ts/proxies/smart-proxy/utils/route-helpers/http-helpers.js +32 -0
  45. package/dist_ts/proxies/smart-proxy/utils/route-helpers/https-helpers.d.ts +68 -0
  46. package/dist_ts/proxies/smart-proxy/utils/route-helpers/https-helpers.js +117 -0
  47. package/dist_ts/proxies/smart-proxy/utils/route-helpers/index.d.ts +17 -0
  48. package/dist_ts/proxies/smart-proxy/utils/route-helpers/index.js +27 -0
  49. package/dist_ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.d.ts +63 -0
  50. package/dist_ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.js +105 -0
  51. package/dist_ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.d.ts +83 -0
  52. package/dist_ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.js +126 -0
  53. package/dist_ts/proxies/smart-proxy/utils/route-helpers/security-helpers.d.ts +47 -0
  54. package/dist_ts/proxies/smart-proxy/utils/route-helpers/security-helpers.js +66 -0
  55. package/dist_ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.d.ts +70 -0
  56. package/dist_ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.js +287 -0
  57. package/dist_ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.d.ts +46 -0
  58. package/dist_ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.js +67 -0
  59. package/dist_ts/proxies/smart-proxy/utils/route-helpers.d.ts +4 -457
  60. package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +6 -950
  61. package/dist_ts/proxies/smart-proxy/utils/route-utils.js +2 -2
  62. package/dist_ts/proxies/smart-proxy/utils/route-validator.d.ts +67 -1
  63. package/dist_ts/proxies/smart-proxy/utils/route-validator.js +266 -6
  64. package/npmextra.json +12 -6
  65. package/package.json +34 -24
  66. package/readme.hints.md +184 -1
  67. package/readme.md +235 -172
  68. package/ts/00_commitinfo_data.ts +1 -1
  69. package/ts/core/utils/shared-security-manager.ts +98 -13
  70. package/ts/proxies/http-proxy/default-certificates.ts +150 -0
  71. package/ts/proxies/http-proxy/http-proxy.ts +9 -15
  72. package/ts/proxies/http-proxy/index.ts +6 -1
  73. package/ts/proxies/http-proxy/security-manager.ts +141 -161
  74. package/ts/proxies/nftables-proxy/index.ts +1 -0
  75. package/ts/proxies/nftables-proxy/nftables-proxy.ts +116 -290
  76. package/ts/proxies/nftables-proxy/utils/index.ts +38 -0
  77. package/ts/proxies/nftables-proxy/utils/nft-command-executor.ts +162 -0
  78. package/ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.ts +125 -0
  79. package/ts/proxies/nftables-proxy/utils/nft-rule-validator.ts +156 -0
  80. package/ts/proxies/smart-proxy/certificate-manager.ts +3 -2
  81. package/ts/proxies/smart-proxy/connection-manager.ts +21 -8
  82. package/ts/proxies/smart-proxy/http-proxy-bridge.ts +39 -13
  83. package/ts/proxies/smart-proxy/models/interfaces.ts +0 -1
  84. package/ts/proxies/smart-proxy/route-connection-handler.ts +88 -16
  85. package/ts/proxies/smart-proxy/security-manager.ts +98 -86
  86. package/ts/proxies/smart-proxy/smart-proxy.ts +0 -2
  87. package/ts/proxies/smart-proxy/tls-manager.ts +1 -37
  88. package/ts/proxies/smart-proxy/utils/index.ts +3 -5
  89. package/ts/proxies/smart-proxy/utils/route-helpers/api-helpers.ts +144 -0
  90. package/ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.ts +124 -0
  91. package/ts/proxies/smart-proxy/utils/route-helpers/http-helpers.ts +40 -0
  92. package/ts/proxies/smart-proxy/utils/route-helpers/https-helpers.ts +163 -0
  93. package/ts/proxies/smart-proxy/utils/route-helpers/index.ts +62 -0
  94. package/ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.ts +154 -0
  95. package/ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.ts +202 -0
  96. package/ts/proxies/smart-proxy/utils/route-helpers/security-helpers.ts +96 -0
  97. package/ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.ts +337 -0
  98. package/ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.ts +98 -0
  99. package/ts/proxies/smart-proxy/utils/route-helpers.ts +5 -1302
  100. package/ts/proxies/smart-proxy/utils/route-utils.ts +1 -1
  101. package/ts/proxies/smart-proxy/utils/route-validator.ts +289 -7
  102. package/ts/proxies/http-proxy/certificate-manager.ts +0 -244
  103. package/ts/proxies/smart-proxy/utils/route-validators.ts +0 -283
@@ -3,10 +3,8 @@ import { promisify } from 'util';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
  import * as os from 'os';
6
- import { delay } from '../../core/utils/async-utils.js';
7
6
  import { AsyncFileSystem } from '../../core/utils/fs-utils.js';
8
7
  import {
9
- NftBaseError,
10
8
  NftValidationError,
11
9
  NftExecutionError,
12
10
  NftResourceError
@@ -16,6 +14,12 @@ import type {
16
14
  NfTableProxyOptions,
17
15
  NfTablesStatus
18
16
  } from './models/index.js';
17
+ import {
18
+ NftCommandExecutor,
19
+ normalizePortSpec,
20
+ validateSettings,
21
+ filterIPsByFamily
22
+ } from './utils/index.js';
19
23
 
20
24
  const execAsync = promisify(exec);
21
25
 
@@ -44,11 +48,12 @@ export class NfTablesProxy {
44
48
  private ruleTag: string;
45
49
  private tableName: string;
46
50
  private tempFilePath: string;
51
+ private executor: NftCommandExecutor;
47
52
  private static NFT_CMD = 'nft';
48
53
 
49
54
  constructor(settings: NfTableProxyOptions) {
50
55
  // Validate inputs to prevent command injection
51
- this.validateSettings(settings);
56
+ validateSettings(settings);
52
57
 
53
58
  // Set default settings
54
59
  this.settings = {
@@ -74,225 +79,57 @@ export class NfTablesProxy {
74
79
  // Create a temp file path for batch operations
75
80
  this.tempFilePath = path.join(os.tmpdir(), `nft-rules-${Date.now()}.nft`);
76
81
 
82
+ // Create the command executor
83
+ this.executor = new NftCommandExecutor(
84
+ (level, message, data) => this.log(level, message, data),
85
+ {
86
+ maxRetries: this.settings.maxRetries,
87
+ retryDelayMs: this.settings.retryDelayMs,
88
+ tempFilePath: this.tempFilePath
89
+ }
90
+ );
91
+
77
92
  // Register cleanup handlers if deleteOnExit is true
78
93
  if (this.settings.deleteOnExit) {
79
- const cleanup = () => {
94
+ // Synchronous cleanup for 'exit' event (only sync code runs here)
95
+ const syncCleanup = () => {
80
96
  try {
81
97
  this.stopSync();
82
98
  } catch (err) {
83
99
  this.log('error', 'Error cleaning nftables rules on exit:', { error: err.message });
84
100
  }
85
101
  };
86
-
87
- process.on('exit', cleanup);
102
+
103
+ // Async cleanup for signal handlers (preferred, non-blocking)
104
+ const asyncCleanup = async () => {
105
+ try {
106
+ await this.stop();
107
+ } catch (err) {
108
+ this.log('error', 'Error cleaning nftables rules on signal:', { error: err.message });
109
+ }
110
+ };
111
+
112
+ process.on('exit', syncCleanup);
88
113
  process.on('SIGINT', () => {
89
- cleanup();
90
- process.exit();
114
+ asyncCleanup().finally(() => process.exit());
91
115
  });
92
116
  process.on('SIGTERM', () => {
93
- cleanup();
94
- process.exit();
117
+ asyncCleanup().finally(() => process.exit());
95
118
  });
96
119
  }
97
120
  }
98
121
 
99
- /**
100
- * Validates settings to prevent command injection and ensure valid values
101
- */
102
- private validateSettings(settings: NfTableProxyOptions): void {
103
- // Validate port numbers
104
- const validatePorts = (port: number | PortRange | Array<number | PortRange>) => {
105
- if (Array.isArray(port)) {
106
- port.forEach(p => validatePorts(p));
107
- return;
108
- }
109
-
110
- if (typeof port === 'number') {
111
- if (port < 1 || port > 65535) {
112
- throw new NftValidationError(`Invalid port number: ${port}`);
113
- }
114
- } else if (typeof port === 'object') {
115
- if (port.from < 1 || port.from > 65535 || port.to < 1 || port.to > 65535 || port.from > port.to) {
116
- throw new NftValidationError(`Invalid port range: ${port.from}-${port.to}`);
117
- }
118
- }
119
- };
120
-
121
- validatePorts(settings.fromPort);
122
- validatePorts(settings.toPort);
123
-
124
- // Define regex patterns for validation
125
- 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]))?$/;
126
- 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]))?$/;
127
-
128
- // Validate IP addresses
129
- const validateIPs = (ips?: string[]) => {
130
- if (!ips) return;
131
-
132
- for (const ip of ips) {
133
- if (!ipRegex.test(ip) && !ipv6Regex.test(ip)) {
134
- throw new NftValidationError(`Invalid IP address format: ${ip}`);
135
- }
136
- }
137
- };
138
-
139
- validateIPs(settings.ipAllowList);
140
- validateIPs(settings.ipBlockList);
141
-
142
- // Validate toHost - only allow hostnames or IPs
143
- if (settings.toHost) {
144
- 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])$/;
145
- if (!hostRegex.test(settings.toHost) && !ipRegex.test(settings.toHost) && !ipv6Regex.test(settings.toHost)) {
146
- throw new NftValidationError(`Invalid host format: ${settings.toHost}`);
147
- }
148
- }
149
-
150
- // Validate table name to prevent command injection
151
- if (settings.tableName) {
152
- const tableNameRegex = /^[a-zA-Z0-9_]+$/;
153
- if (!tableNameRegex.test(settings.tableName)) {
154
- throw new NftValidationError(`Invalid table name: ${settings.tableName}. Only alphanumeric characters and underscores are allowed.`);
155
- }
156
- }
157
-
158
- // Validate QoS settings if enabled
159
- if (settings.qos?.enabled) {
160
- if (settings.qos.maxRate) {
161
- const rateRegex = /^[0-9]+[kKmMgG]?bps$/;
162
- if (!rateRegex.test(settings.qos.maxRate)) {
163
- throw new NftValidationError(`Invalid rate format: ${settings.qos.maxRate}. Use format like "10mbps", "1gbps", etc.`);
164
- }
165
- }
166
-
167
- if (settings.qos.priority !== undefined) {
168
- if (settings.qos.priority < 1 || settings.qos.priority > 10 || !Number.isInteger(settings.qos.priority)) {
169
- throw new NftValidationError(`Invalid priority: ${settings.qos.priority}. Must be an integer between 1 and 10.`);
170
- }
171
- }
172
- }
173
- }
174
-
175
- /**
176
- * Normalizes port specifications into an array of port ranges
177
- */
178
- private normalizePortSpec(portSpec: number | PortRange | Array<number | PortRange>): PortRange[] {
179
- const result: PortRange[] = [];
180
-
181
- if (Array.isArray(portSpec)) {
182
- // If it's an array, process each element
183
- for (const spec of portSpec) {
184
- result.push(...this.normalizePortSpec(spec));
185
- }
186
- } else if (typeof portSpec === 'number') {
187
- // Single port becomes a range with the same start and end
188
- result.push({ from: portSpec, to: portSpec });
189
- } else {
190
- // Already a range
191
- result.push(portSpec);
192
- }
193
-
194
- return result;
195
- }
196
-
197
- /**
198
- * Execute a command with retry capability
199
- */
200
- private async executeWithRetry(command: string, maxRetries = 3, retryDelayMs = 1000): Promise<string> {
201
- let lastError: Error | undefined;
202
-
203
- for (let i = 0; i < maxRetries; i++) {
204
- try {
205
- const { stdout } = await execAsync(command);
206
- return stdout;
207
- } catch (err) {
208
- lastError = err;
209
- this.log('warn', `Command failed (attempt ${i+1}/${maxRetries}): ${command}`, { error: err.message });
210
-
211
- // Wait before retry, unless it's the last attempt
212
- if (i < maxRetries - 1) {
213
- await delay(retryDelayMs);
214
- }
215
- }
216
- }
217
-
218
- throw new NftExecutionError(`Failed after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
219
- }
220
-
221
- /**
222
- * Execute system command synchronously with multiple attempts
223
- * @deprecated This method blocks the event loop and should be avoided. Use executeWithRetry instead.
224
- * WARNING: This method contains a busy wait loop that will block the entire Node.js event loop!
225
- */
226
- private executeWithRetrySync(command: string, maxRetries = 3, retryDelayMs = 1000): string {
227
- // Log deprecation warning
228
- console.warn('[DEPRECATION WARNING] executeWithRetrySync blocks the event loop and should not be used. Consider using the async executeWithRetry method instead.');
229
-
230
- let lastError: Error | undefined;
231
-
232
- for (let i = 0; i < maxRetries; i++) {
233
- try {
234
- return execSync(command).toString();
235
- } catch (err) {
236
- lastError = err;
237
- this.log('warn', `Command failed (attempt ${i+1}/${maxRetries}): ${command}`, { error: err.message });
238
-
239
- // Wait before retry, unless it's the last attempt
240
- if (i < maxRetries - 1) {
241
- // CRITICAL: This busy wait loop blocks the entire event loop!
242
- // This is a temporary fallback for sync contexts only.
243
- // TODO: Remove this method entirely and make all callers async
244
- const waitUntil = Date.now() + retryDelayMs;
245
- while (Date.now() < waitUntil) {
246
- // Busy wait - blocks event loop
247
- }
248
- }
249
- }
250
- }
251
-
252
- throw new NftExecutionError(`Failed after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
253
- }
254
-
255
- /**
256
- * Execute nftables commands with a temporary file
257
- * This helper handles the common pattern of writing rules to a temp file,
258
- * executing nftables with the file, and cleaning up
259
- */
260
- private async executeWithTempFile(rulesetContent: string): Promise<void> {
261
- await AsyncFileSystem.writeFile(this.tempFilePath, rulesetContent);
262
-
263
- try {
264
- await this.executeWithRetry(
265
- `${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
266
- this.settings.maxRetries,
267
- this.settings.retryDelayMs
268
- );
269
- } finally {
270
- // Always clean up the temp file
271
- await AsyncFileSystem.remove(this.tempFilePath);
272
- }
273
- }
274
-
275
122
  /**
276
123
  * Checks if nftables is available and the required modules are loaded
277
124
  */
278
125
  private async checkNftablesAvailability(): Promise<boolean> {
279
- try {
280
- await this.executeWithRetry(`${NfTablesProxy.NFT_CMD} --version`, this.settings.maxRetries, this.settings.retryDelayMs);
281
-
282
- // Check for conntrack support if we're using advanced NAT
283
- if (this.settings.useAdvancedNAT) {
284
- try {
285
- await this.executeWithRetry('lsmod | grep nf_conntrack', this.settings.maxRetries, this.settings.retryDelayMs);
286
- } catch (err) {
287
- this.log('warn', 'Connection tracking modules might not be loaded, advanced NAT features may not work');
288
- }
289
- }
290
-
291
- return true;
292
- } catch (err) {
293
- this.log('error', `nftables is not available: ${err.message}`);
294
- return false;
126
+ const available = await this.executor.checkAvailability();
127
+
128
+ if (available && this.settings.useAdvancedNAT) {
129
+ await this.executor.checkConntrackModules();
295
130
  }
131
+
132
+ return available;
296
133
  }
297
134
 
298
135
  /**
@@ -303,7 +140,7 @@ export class NfTablesProxy {
303
140
 
304
141
  try {
305
142
  // Check if the table already exists
306
- const stdout = await this.executeWithRetry(
143
+ const stdout = await this.executor.executeWithRetry(
307
144
  `${NfTablesProxy.NFT_CMD} list tables ${family}`,
308
145
  this.settings.maxRetries,
309
146
  this.settings.retryDelayMs
@@ -313,7 +150,7 @@ export class NfTablesProxy {
313
150
 
314
151
  if (!tableExists) {
315
152
  // Create the table
316
- await this.executeWithRetry(
153
+ await this.executor.executeWithRetry(
317
154
  `${NfTablesProxy.NFT_CMD} add table ${family} ${this.tableName}`,
318
155
  this.settings.maxRetries,
319
156
  this.settings.retryDelayMs
@@ -322,7 +159,7 @@ export class NfTablesProxy {
322
159
  this.log('info', `Created table ${family} ${this.tableName}`);
323
160
 
324
161
  // Create the nat chain for the prerouting hook
325
- await this.executeWithRetry(
162
+ await this.executor.executeWithRetry(
326
163
  `${NfTablesProxy.NFT_CMD} add chain ${family} ${this.tableName} nat_prerouting { type nat hook prerouting priority -100 ; }`,
327
164
  this.settings.maxRetries,
328
165
  this.settings.retryDelayMs
@@ -332,7 +169,7 @@ export class NfTablesProxy {
332
169
 
333
170
  // Create the nat chain for the postrouting hook if not preserving source IP
334
171
  if (!this.settings.preserveSourceIP) {
335
- await this.executeWithRetry(
172
+ await this.executor.executeWithRetry(
336
173
  `${NfTablesProxy.NFT_CMD} add chain ${family} ${this.tableName} nat_postrouting { type nat hook postrouting priority 100 ; }`,
337
174
  this.settings.maxRetries,
338
175
  this.settings.retryDelayMs
@@ -343,7 +180,7 @@ export class NfTablesProxy {
343
180
 
344
181
  // Create the chain for NetworkProxy integration if needed
345
182
  if (this.settings.netProxyIntegration?.enabled && this.settings.netProxyIntegration.redirectLocalhost) {
346
- await this.executeWithRetry(
183
+ await this.executor.executeWithRetry(
347
184
  `${NfTablesProxy.NFT_CMD} add chain ${family} ${this.tableName} nat_output { type nat hook output priority 0 ; }`,
348
185
  this.settings.maxRetries,
349
186
  this.settings.retryDelayMs
@@ -354,7 +191,7 @@ export class NfTablesProxy {
354
191
 
355
192
  // Create the QoS chain if needed
356
193
  if (this.settings.qos?.enabled) {
357
- await this.executeWithRetry(
194
+ await this.executor.executeWithRetry(
358
195
  `${NfTablesProxy.NFT_CMD} add chain ${family} ${this.tableName} qos_forward { type filter hook forward priority 0 ; }`,
359
196
  this.settings.maxRetries,
360
197
  this.settings.retryDelayMs
@@ -384,11 +221,7 @@ export class NfTablesProxy {
384
221
  ): Promise<boolean> {
385
222
  try {
386
223
  // Filter IPs based on family
387
- const filteredIPs = ips.filter(ip => {
388
- if (family === 'ip6' && ip.includes(':')) return true;
389
- if (family === 'ip' && ip.includes('.')) return true;
390
- return false;
391
- });
224
+ const filteredIPs = filterIPsByFamily(ips, family as 'ip' | 'ip6');
392
225
 
393
226
  if (filteredIPs.length === 0) {
394
227
  this.log('info', `No IP addresses of type ${setType} to add to set ${setName}`);
@@ -397,7 +230,7 @@ export class NfTablesProxy {
397
230
 
398
231
  // Check if set already exists
399
232
  try {
400
- const sets = await this.executeWithRetry(
233
+ const sets = await this.executor.executeWithRetry(
401
234
  `${NfTablesProxy.NFT_CMD} list sets ${family} ${this.tableName}`,
402
235
  this.settings.maxRetries,
403
236
  this.settings.retryDelayMs
@@ -407,7 +240,7 @@ export class NfTablesProxy {
407
240
  this.log('info', `IP set ${setName} already exists, will add elements`);
408
241
  } else {
409
242
  // Create the set
410
- await this.executeWithRetry(
243
+ await this.executor.executeWithRetry(
411
244
  `${NfTablesProxy.NFT_CMD} add set ${family} ${this.tableName} ${setName} { type ${setType}; }`,
412
245
  this.settings.maxRetries,
413
246
  this.settings.retryDelayMs
@@ -417,7 +250,7 @@ export class NfTablesProxy {
417
250
  }
418
251
  } catch (err) {
419
252
  // Set might not exist yet, create it
420
- await this.executeWithRetry(
253
+ await this.executor.executeWithRetry(
421
254
  `${NfTablesProxy.NFT_CMD} add set ${family} ${this.tableName} ${setName} { type ${setType}; }`,
422
255
  this.settings.maxRetries,
423
256
  this.settings.retryDelayMs
@@ -432,7 +265,7 @@ export class NfTablesProxy {
432
265
  const batch = filteredIPs.slice(i, i + batchSize);
433
266
  const elements = batch.join(', ');
434
267
 
435
- await this.executeWithRetry(
268
+ await this.executor.executeWithRetry(
436
269
  `${NfTablesProxy.NFT_CMD} add element ${family} ${this.tableName} ${setName} { ${elements} }`,
437
270
  this.settings.maxRetries,
438
271
  this.settings.retryDelayMs
@@ -575,7 +408,7 @@ export class NfTablesProxy {
575
408
  // Only write and apply if we have rules to add
576
409
  if (rulesetContent) {
577
410
  // Apply the ruleset using the helper
578
- await this.executeWithTempFile(rulesetContent);
411
+ await this.executor.executeWithTempFile(rulesetContent);
579
412
 
580
413
  this.log('info', `Added source IP filter rules for ${family}`);
581
414
 
@@ -605,7 +438,7 @@ export class NfTablesProxy {
605
438
  * Gets a comma-separated list of all ports from a port specification
606
439
  */
607
440
  private getAllPorts(portSpec: number | PortRange | Array<number | PortRange>): string {
608
- const portRanges = this.normalizePortSpec(portSpec);
441
+ const portRanges = normalizePortSpec(portSpec);
609
442
  const ports: string[] = [];
610
443
 
611
444
  for (const range of portRanges) {
@@ -632,8 +465,8 @@ export class NfTablesProxy {
632
465
 
633
466
  try {
634
467
  // Get the port ranges
635
- const fromPortRanges = this.normalizePortSpec(this.settings.fromPort);
636
- const toPortRanges = this.normalizePortSpec(this.settings.toPort);
468
+ const fromPortRanges = normalizePortSpec(this.settings.fromPort);
469
+ const toPortRanges = normalizePortSpec(this.settings.toPort);
637
470
 
638
471
  let rulesetContent = '';
639
472
 
@@ -682,7 +515,7 @@ export class NfTablesProxy {
682
515
 
683
516
  // Apply the rules if we have any
684
517
  if (rulesetContent) {
685
- await this.executeWithTempFile(rulesetContent);
518
+ await this.executor.executeWithTempFile(rulesetContent);
686
519
 
687
520
  this.log('info', `Added advanced NAT rules for ${family}`);
688
521
 
@@ -720,8 +553,8 @@ export class NfTablesProxy {
720
553
 
721
554
  try {
722
555
  // Normalize port specifications
723
- const fromPortRanges = this.normalizePortSpec(this.settings.fromPort);
724
- const toPortRanges = this.normalizePortSpec(this.settings.toPort);
556
+ const fromPortRanges = normalizePortSpec(this.settings.fromPort);
557
+ const toPortRanges = normalizePortSpec(this.settings.toPort);
725
558
 
726
559
  // Handle the case where fromPort and toPort counts don't match
727
560
  if (fromPortRanges.length !== toPortRanges.length) {
@@ -827,7 +660,7 @@ export class NfTablesProxy {
827
660
  // Apply the ruleset if we have any rules
828
661
  if (rulesetContent) {
829
662
  // Apply the ruleset using the helper
830
- await this.executeWithTempFile(rulesetContent);
663
+ await this.executor.executeWithTempFile(rulesetContent);
831
664
 
832
665
  this.log('info', `Added port forwarding rules for ${family}`);
833
666
 
@@ -931,7 +764,7 @@ export class NfTablesProxy {
931
764
 
932
765
  // Apply the ruleset if we have any rules
933
766
  if (rulesetContent) {
934
- await this.executeWithTempFile(rulesetContent);
767
+ await this.executor.executeWithTempFile(rulesetContent);
935
768
 
936
769
  this.log('info', `Added port forwarding rules for ${family}`);
937
770
 
@@ -984,7 +817,7 @@ export class NfTablesProxy {
984
817
  // Add priority marking if specified
985
818
  if (this.settings.qos.priority !== undefined) {
986
819
  // Check if the chain exists
987
- const chainsOutput = await this.executeWithRetry(
820
+ const chainsOutput = await this.executor.executeWithRetry(
988
821
  `${NfTablesProxy.NFT_CMD} list chains ${family} ${this.tableName}`,
989
822
  this.settings.maxRetries,
990
823
  this.settings.retryDelayMs
@@ -1000,7 +833,7 @@ export class NfTablesProxy {
1000
833
  }
1001
834
 
1002
835
  // Add the rules to mark packets with this priority
1003
- for (const range of this.normalizePortSpec(this.settings.toPort)) {
836
+ for (const range of normalizePortSpec(this.settings.toPort)) {
1004
837
  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"`;
1005
838
  rulesetContent += `${markRule}\n`;
1006
839
 
@@ -1017,7 +850,7 @@ export class NfTablesProxy {
1017
850
  // Apply the ruleset if we have any rules
1018
851
  if (rulesetContent) {
1019
852
  // Apply the ruleset using the helper
1020
- await this.executeWithTempFile(rulesetContent);
853
+ await this.executor.executeWithTempFile(rulesetContent);
1021
854
 
1022
855
  this.log('info', `Added QoS rules for ${family}`);
1023
856
 
@@ -1060,7 +893,7 @@ export class NfTablesProxy {
1060
893
  const rule = `add rule ${family} ${this.tableName} ${outputChain} ${this.settings.protocol} daddr ${localhost} redirect to :${netProxyConfig.sslTerminationPort} comment "${this.ruleTag}:NETPROXY_REDIRECT"`;
1061
894
 
1062
895
  // Apply the rule
1063
- await this.executeWithRetry(
896
+ await this.executor.executeWithRetry(
1064
897
  `${NfTablesProxy.NFT_CMD} ${rule}`,
1065
898
  this.settings.maxRetries,
1066
899
  this.settings.retryDelayMs
@@ -1103,7 +936,7 @@ export class NfTablesProxy {
1103
936
  const commentTag = commentMatch[1];
1104
937
 
1105
938
  // List the chain to check if our rule is there
1106
- const stdout = await this.executeWithRetry(
939
+ const stdout = await this.executor.executeWithRetry(
1107
940
  `${NfTablesProxy.NFT_CMD} list chain ${tableFamily} ${tableName} ${chainName}`,
1108
941
  this.settings.maxRetries,
1109
942
  this.settings.retryDelayMs
@@ -1139,7 +972,7 @@ export class NfTablesProxy {
1139
972
  try {
1140
973
  // For nftables, create a delete rule by replacing 'add' with 'delete'
1141
974
  const deleteRule = rule.ruleContents.replace('add rule', 'delete rule');
1142
- await this.executeWithRetry(
975
+ await this.executor.executeWithRetry(
1143
976
  `${NfTablesProxy.NFT_CMD} ${deleteRule}`,
1144
977
  this.settings.maxRetries,
1145
978
  this.settings.retryDelayMs
@@ -1161,7 +994,7 @@ export class NfTablesProxy {
1161
994
  */
1162
995
  private async tableExists(family: string, tableName: string): Promise<boolean> {
1163
996
  try {
1164
- const stdout = await this.executeWithRetry(
997
+ const stdout = await this.executor.executeWithRetry(
1165
998
  `${NfTablesProxy.NFT_CMD} list tables ${family}`,
1166
999
  this.settings.maxRetries,
1167
1000
  this.settings.retryDelayMs
@@ -1190,7 +1023,7 @@ export class NfTablesProxy {
1190
1023
  try {
1191
1024
  // Try to get connection metrics if conntrack is available
1192
1025
  try {
1193
- const stdout = await this.executeWithRetry('conntrack -C', this.settings.maxRetries, this.settings.retryDelayMs);
1026
+ const stdout = await this.executor.executeWithRetry('conntrack -C', this.settings.maxRetries, this.settings.retryDelayMs);
1194
1027
  metrics.activeConnections = parseInt(stdout.trim(), 10);
1195
1028
  } catch (err) {
1196
1029
  // conntrack not available, skip this metric
@@ -1199,7 +1032,7 @@ export class NfTablesProxy {
1199
1032
  // Try to get forwarded connections count from nftables counters
1200
1033
  try {
1201
1034
  // Look for counters in our rules
1202
- const stdout = await this.executeWithRetry(
1035
+ const stdout = await this.executor.executeWithRetry(
1203
1036
  `${NfTablesProxy.NFT_CMD} list table ip ${this.tableName}`,
1204
1037
  this.settings.maxRetries,
1205
1038
  this.settings.retryDelayMs
@@ -1250,7 +1083,7 @@ export class NfTablesProxy {
1250
1083
  try {
1251
1084
  for (const family of ['ip', 'ip6']) {
1252
1085
  try {
1253
- const stdout = await this.executeWithRetry(
1086
+ const stdout = await this.executor.executeWithRetry(
1254
1087
  `${NfTablesProxy.NFT_CMD} list sets ${family} ${this.tableName}`,
1255
1088
  this.settings.maxRetries,
1256
1089
  this.settings.retryDelayMs
@@ -1302,7 +1135,7 @@ export class NfTablesProxy {
1302
1135
 
1303
1136
  try {
1304
1137
  // Get list of configured tables
1305
- const stdout = await this.executeWithRetry(
1138
+ const stdout = await this.executor.executeWithRetry(
1306
1139
  `${NfTablesProxy.NFT_CMD} list tables`,
1307
1140
  this.settings.maxRetries,
1308
1141
  this.settings.retryDelayMs
@@ -1408,8 +1241,8 @@ export class NfTablesProxy {
1408
1241
  // Port forwarding rules
1409
1242
  if (this.settings.useAdvancedNAT) {
1410
1243
  // Advanced NAT with connection tracking
1411
- const fromPortRanges = this.normalizePortSpec(this.settings.fromPort);
1412
- const toPortRanges = this.normalizePortSpec(this.settings.toPort);
1244
+ const fromPortRanges = normalizePortSpec(this.settings.fromPort);
1245
+ const toPortRanges = normalizePortSpec(this.settings.toPort);
1413
1246
 
1414
1247
  if (fromPortRanges.length === 1 && toPortRanges.length === 1) {
1415
1248
  const fromRange = fromPortRanges[0];
@@ -1425,8 +1258,8 @@ export class NfTablesProxy {
1425
1258
  }
1426
1259
  } else {
1427
1260
  // Standard NAT rules
1428
- const fromRanges = this.normalizePortSpec(this.settings.fromPort);
1429
- const toRanges = this.normalizePortSpec(this.settings.toPort);
1261
+ const fromRanges = normalizePortSpec(this.settings.fromPort);
1262
+ const toRanges = normalizePortSpec(this.settings.toPort);
1430
1263
 
1431
1264
  if (fromRanges.length === 1 && toRanges.length === 1) {
1432
1265
  const fromRange = fromRanges[0];
@@ -1472,7 +1305,7 @@ export class NfTablesProxy {
1472
1305
  if (this.settings.qos.priority !== undefined) {
1473
1306
  commands.push(`add chain ip ${this.tableName} prio${this.settings.qos.priority} { type filter hook forward priority ${this.settings.qos.priority * 10}; }`);
1474
1307
 
1475
- for (const range of this.normalizePortSpec(this.settings.toPort)) {
1308
+ for (const range of normalizePortSpec(this.settings.toPort)) {
1476
1309
  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"`);
1477
1310
  }
1478
1311
  }
@@ -1598,7 +1431,7 @@ export class NfTablesProxy {
1598
1431
 
1599
1432
  try {
1600
1433
  // Apply the ruleset
1601
- await this.executeWithRetry(
1434
+ await this.executor.executeWithRetry(
1602
1435
  `${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
1603
1436
  this.settings.maxRetries,
1604
1437
  this.settings.retryDelayMs
@@ -1623,7 +1456,7 @@ export class NfTablesProxy {
1623
1456
  const [family, setName] = key.split(':');
1624
1457
 
1625
1458
  try {
1626
- await this.executeWithRetry(
1459
+ await this.executor.executeWithRetry(
1627
1460
  `${NfTablesProxy.NFT_CMD} delete set ${family} ${this.tableName} ${setName}`,
1628
1461
  this.settings.maxRetries,
1629
1462
  this.settings.retryDelayMs
@@ -1649,67 +1482,66 @@ export class NfTablesProxy {
1649
1482
  }
1650
1483
 
1651
1484
  /**
1652
- * Synchronous version of stop, for use in exit handlers
1485
+ * Synchronous version of stop, for use in exit handlers only.
1486
+ * Uses single-attempt commands without retry (process is exiting anyway).
1653
1487
  */
1654
1488
  public stopSync(): void {
1655
1489
  try {
1656
1490
  let rulesetContent = '';
1657
-
1491
+
1658
1492
  // Process rules in reverse order (LIFO)
1659
1493
  for (let i = this.rules.length - 1; i >= 0; i--) {
1660
1494
  const rule = this.rules[i];
1661
-
1495
+
1662
1496
  if (rule.added) {
1663
1497
  // Create delete rules by replacing 'add' with 'delete'
1664
1498
  const deleteRule = rule.ruleContents.replace('add rule', 'delete rule');
1665
1499
  rulesetContent += `${deleteRule}\n`;
1666
1500
  }
1667
1501
  }
1668
-
1502
+
1669
1503
  // Apply the ruleset if we have any rules to delete
1670
1504
  if (rulesetContent) {
1671
1505
  // Write to temporary file
1672
1506
  fs.writeFileSync(this.tempFilePath, rulesetContent);
1673
-
1674
- // Apply the ruleset
1675
- this.executeWithRetrySync(
1676
- `${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
1677
- this.settings.maxRetries,
1678
- this.settings.retryDelayMs
1679
- );
1680
-
1507
+
1508
+ // Apply the ruleset (single attempt, no retry - process is exiting)
1509
+ this.executor.executeSync(`${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`);
1510
+
1681
1511
  this.log('info', 'Removed all added rules');
1682
-
1512
+
1683
1513
  // Mark all rules as removed
1684
1514
  this.rules.forEach(rule => {
1685
1515
  rule.added = false;
1686
1516
  rule.verified = false;
1687
1517
  });
1688
-
1518
+
1689
1519
  // Remove temporary file
1690
- fs.unlinkSync(this.tempFilePath);
1520
+ try {
1521
+ fs.unlinkSync(this.tempFilePath);
1522
+ } catch {
1523
+ // Ignore - process is exiting
1524
+ }
1691
1525
  }
1692
-
1526
+
1693
1527
  // Clean up IP sets if we created any
1694
1528
  if (this.settings.useIPSets && this.ipSets.size > 0) {
1695
1529
  for (const [key, _] of this.ipSets) {
1696
1530
  const [family, setName] = key.split(':');
1697
-
1531
+
1698
1532
  try {
1699
- this.executeWithRetrySync(
1700
- `${NfTablesProxy.NFT_CMD} delete set ${family} ${this.tableName} ${setName}`,
1701
- this.settings.maxRetries,
1702
- this.settings.retryDelayMs
1533
+ this.executor.executeSync(
1534
+ `${NfTablesProxy.NFT_CMD} delete set ${family} ${this.tableName} ${setName}`
1703
1535
  );
1704
- } catch (err) {
1536
+ } catch {
1705
1537
  // Non-critical error, continue
1706
1538
  }
1707
1539
  }
1708
1540
  }
1709
-
1541
+
1710
1542
  // Optionally clean up tables if they're empty (sync version)
1711
1543
  this.cleanupEmptyTablesSync();
1712
-
1544
+
1713
1545
  this.log('info', 'NfTablesProxy stopped successfully');
1714
1546
  } catch (err) {
1715
1547
  this.log('error', `Error stopping NfTablesProxy: ${err.message}`);
@@ -1735,7 +1567,7 @@ export class NfTablesProxy {
1735
1567
  }
1736
1568
 
1737
1569
  // Check if the table has any rules
1738
- const stdout = await this.executeWithRetry(
1570
+ const stdout = await this.executor.executeWithRetry(
1739
1571
  `${NfTablesProxy.NFT_CMD} list table ${family} ${this.tableName}`,
1740
1572
  this.settings.maxRetries,
1741
1573
  this.settings.retryDelayMs
@@ -1745,7 +1577,7 @@ export class NfTablesProxy {
1745
1577
 
1746
1578
  if (!hasRules) {
1747
1579
  // Table is empty, delete it
1748
- await this.executeWithRetry(
1580
+ await this.executor.executeWithRetry(
1749
1581
  `${NfTablesProxy.NFT_CMD} delete table ${family} ${this.tableName}`,
1750
1582
  this.settings.maxRetries,
1751
1583
  this.settings.retryDelayMs
@@ -1760,7 +1592,7 @@ export class NfTablesProxy {
1760
1592
  }
1761
1593
 
1762
1594
  /**
1763
- * Synchronous version of cleanupEmptyTables
1595
+ * Synchronous version of cleanupEmptyTables (for exit handlers only)
1764
1596
  */
1765
1597
  private cleanupEmptyTablesSync(): void {
1766
1598
  // Check if tables are empty, and if so, delete them
@@ -1769,38 +1601,32 @@ export class NfTablesProxy {
1769
1601
  if (family === 'ip6' && !this.settings.ipv6Support) {
1770
1602
  continue;
1771
1603
  }
1772
-
1604
+
1773
1605
  try {
1774
1606
  // Check if table exists
1775
- const tableExistsOutput = this.executeWithRetrySync(
1776
- `${NfTablesProxy.NFT_CMD} list tables ${family}`,
1777
- this.settings.maxRetries,
1778
- this.settings.retryDelayMs
1607
+ const tableExistsOutput = this.executor.executeSync(
1608
+ `${NfTablesProxy.NFT_CMD} list tables ${family}`
1779
1609
  );
1780
-
1610
+
1781
1611
  const tableExists = tableExistsOutput.includes(`table ${family} ${this.tableName}`);
1782
-
1612
+
1783
1613
  if (!tableExists) {
1784
1614
  continue;
1785
1615
  }
1786
-
1616
+
1787
1617
  // Check if the table has any rules
1788
- const stdout = this.executeWithRetrySync(
1789
- `${NfTablesProxy.NFT_CMD} list table ${family} ${this.tableName}`,
1790
- this.settings.maxRetries,
1791
- this.settings.retryDelayMs
1618
+ const stdout = this.executor.executeSync(
1619
+ `${NfTablesProxy.NFT_CMD} list table ${family} ${this.tableName}`
1792
1620
  );
1793
-
1621
+
1794
1622
  const hasRules = stdout.includes('rule');
1795
-
1623
+
1796
1624
  if (!hasRules) {
1797
1625
  // Table is empty, delete it
1798
- this.executeWithRetrySync(
1799
- `${NfTablesProxy.NFT_CMD} delete table ${family} ${this.tableName}`,
1800
- this.settings.maxRetries,
1801
- this.settings.retryDelayMs
1626
+ this.executor.executeSync(
1627
+ `${NfTablesProxy.NFT_CMD} delete table ${family} ${this.tableName}`
1802
1628
  );
1803
-
1629
+
1804
1630
  this.log('info', `Deleted empty table ${family} ${this.tableName}`);
1805
1631
  }
1806
1632
  } catch (err) {