@vpalmisano/throttler 0.0.7
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/LICENSE +661 -0
- package/README.md +40 -0
- package/build/src/app.d.ts +1 -0
- package/build/src/app.js +134 -0
- package/build/src/app.js.map +1 -0
- package/build/src/config.d.ts +21 -0
- package/build/src/config.js +199 -0
- package/build/src/config.js.map +1 -0
- package/build/src/generate-config-docs.d.ts +1 -0
- package/build/src/generate-config-docs.js +43 -0
- package/build/src/generate-config-docs.js.map +1 -0
- package/build/src/index.d.ts +4 -0
- package/build/src/index.js +21 -0
- package/build/src/index.js.map +1 -0
- package/build/src/throttle.d.ts +85 -0
- package/build/src/throttle.js +385 -0
- package/build/src/throttle.js.map +1 -0
- package/build/src/utils.d.ts +57 -0
- package/build/src/utils.js +223 -0
- package/build/src/utils.js.map +1 -0
- package/build/tsconfig.tsbuildinfo +1 -0
- package/package.json +78 -0
- package/src/app.ts +151 -0
- package/src/config.ts +179 -0
- package/src/generate-config-docs.ts +51 -0
- package/src/index.ts +4 -0
- package/src/throttle.ts +562 -0
- package/src/utils.ts +249 -0
- package/throttler.js +2 -0
package/src/throttle.ts
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { spawn } from 'child_process'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import JSON5 from 'json5'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
checkNetworkInterface,
|
|
8
|
+
getDefaultNetworkInterface,
|
|
9
|
+
logger,
|
|
10
|
+
runShellCommand,
|
|
11
|
+
toPrecision,
|
|
12
|
+
} from './utils'
|
|
13
|
+
|
|
14
|
+
const log = logger('throttler:throttle')
|
|
15
|
+
|
|
16
|
+
let throttleConfig: ThrottleConfig[] | null = null
|
|
17
|
+
|
|
18
|
+
const ruleTimeouts = new Set<NodeJS.Timeout>()
|
|
19
|
+
|
|
20
|
+
const captureStops = new Map<number, () => Promise<void>>()
|
|
21
|
+
|
|
22
|
+
const throttleCurrentValues = {
|
|
23
|
+
up: new Map<
|
|
24
|
+
number,
|
|
25
|
+
{
|
|
26
|
+
rate?: number
|
|
27
|
+
delay?: number
|
|
28
|
+
delayJitter?: number
|
|
29
|
+
delayJitterCorrelation?: number
|
|
30
|
+
loss?: number
|
|
31
|
+
lossBurst?: number
|
|
32
|
+
queue?: number
|
|
33
|
+
}
|
|
34
|
+
>(),
|
|
35
|
+
down: new Map<
|
|
36
|
+
number,
|
|
37
|
+
{
|
|
38
|
+
rate?: number
|
|
39
|
+
delay?: number
|
|
40
|
+
delayJitterCorrelation?: number
|
|
41
|
+
loss?: number
|
|
42
|
+
lossBurst?: number
|
|
43
|
+
queue?: number
|
|
44
|
+
}
|
|
45
|
+
>(),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function cleanup(): Promise<void> {
|
|
49
|
+
await Promise.allSettled([...captureStops.values()].map(stop => stop()))
|
|
50
|
+
captureStops.clear()
|
|
51
|
+
ruleTimeouts.forEach(timeoutId => clearTimeout(timeoutId))
|
|
52
|
+
ruleTimeouts.clear()
|
|
53
|
+
throttleCurrentValues.up.clear()
|
|
54
|
+
throttleCurrentValues.down.clear()
|
|
55
|
+
let device = throttleConfig?.length ? throttleConfig[0].device : ''
|
|
56
|
+
if (!device) {
|
|
57
|
+
device = await getDefaultNetworkInterface()
|
|
58
|
+
}
|
|
59
|
+
await runShellCommand(`\
|
|
60
|
+
sudo -n tc qdisc del dev ${device} root || true;
|
|
61
|
+
sudo -n tc class del dev ${device} || true;
|
|
62
|
+
sudo -n tc filter del dev ${device} || true;
|
|
63
|
+
sudo -n tc qdisc del dev ${device} ingress || true;
|
|
64
|
+
|
|
65
|
+
sudo -n tc qdisc del dev ifb0 root || true;
|
|
66
|
+
sudo -n tc class del dev ifb0 root || true;
|
|
67
|
+
sudo -n tc filter del dev ifb0 root || true;
|
|
68
|
+
`)
|
|
69
|
+
await cleanupRules()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function calculateBufferedPackets(
|
|
73
|
+
rate: number,
|
|
74
|
+
delay: number,
|
|
75
|
+
mtu = 1500,
|
|
76
|
+
): number {
|
|
77
|
+
// https://lists.linuxfoundation.org/pipermail/netem/2007-March/001094.html
|
|
78
|
+
return Math.ceil((((1.5 * rate * 1000) / 8) * (delay / 1000)) / mtu)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The network throttle rules to be applied to uplink or downlink. */
|
|
82
|
+
export type ThrottleRule = {
|
|
83
|
+
/** The available bandwidth (Kbps). */
|
|
84
|
+
rate?: number
|
|
85
|
+
/** The one-way delay (ms). */
|
|
86
|
+
delay?: number
|
|
87
|
+
/** The one-way delay jitter (ms). */
|
|
88
|
+
delayJitter?: number
|
|
89
|
+
/** The one-way delay jitter correlation. */
|
|
90
|
+
delayJitterCorrelation?: number
|
|
91
|
+
/** The delay distribution. */
|
|
92
|
+
delayDistribution?: 'uniform' | 'normal' | 'pareto' | 'paretonormal'
|
|
93
|
+
/** The packet reordering percentage. */
|
|
94
|
+
reorder?: number
|
|
95
|
+
/** The packet reordering correlation. */
|
|
96
|
+
reorderCorrelation?: number
|
|
97
|
+
/** The packet reordering gap. */
|
|
98
|
+
reorderGap?: number
|
|
99
|
+
/** The packet loss percentage. */
|
|
100
|
+
loss?: number
|
|
101
|
+
/** The packet loss burst. */
|
|
102
|
+
lossBurst?: number
|
|
103
|
+
/** The packet queue size. */
|
|
104
|
+
queue?: number
|
|
105
|
+
/** If set, the rule will be applied after the specified number of seconds. */
|
|
106
|
+
at?: number
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The network throttling rules.
|
|
111
|
+
* Specify multiple {@link ThrottleRule} with different `at` values to schedule
|
|
112
|
+
* network bandwidth/delay fluctuations during the test run, e.g.:
|
|
113
|
+
*
|
|
114
|
+
* ```javascript
|
|
115
|
+
* {
|
|
116
|
+
device: "eth0",
|
|
117
|
+
sessions: "0-1",
|
|
118
|
+
protocol: "udp",
|
|
119
|
+
down: [
|
|
120
|
+
{ rate: 1000000, delay: 50, loss: 0, queue: 5 },
|
|
121
|
+
{ rate: 200000, delay: 100, loss: 5, queue: 5, at: 60},
|
|
122
|
+
],
|
|
123
|
+
up: { rate: 100000, delay: 50, queue: 5 },
|
|
124
|
+
capture: 'capture.pcap',
|
|
125
|
+
}
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export type ThrottleConfig = {
|
|
129
|
+
/** The network interface to throttle. If not specified, the default interface will be used. */
|
|
130
|
+
device?: string
|
|
131
|
+
/** The sessions to throttle. It could be a single index ("0"), a range ("0-2") or a comma-separated list ("0,3,4"). */
|
|
132
|
+
sessions?: string
|
|
133
|
+
/** The protocol to throttle. */
|
|
134
|
+
protocol?: 'udp' | 'tcp'
|
|
135
|
+
/** A comma-separated list of source ports that will not be throttled. */
|
|
136
|
+
skipSourcePorts?: string
|
|
137
|
+
/** A comma-separated list of destination ports that will not be throttled. */
|
|
138
|
+
skipDestinationPorts?: string
|
|
139
|
+
/** An additional IPTables packet filter rule. */
|
|
140
|
+
filter?: string
|
|
141
|
+
/** An additional TC match expression used to filter packets (https://man7.org/linux/man-pages/man8/tc-ematch.8.html). */
|
|
142
|
+
match?: string
|
|
143
|
+
/** If set, the packets matching the provided session and protocol will be captured at that file location. */
|
|
144
|
+
capture?: string
|
|
145
|
+
/** The uplink throttle rules. */
|
|
146
|
+
up?: ThrottleRule | ThrottleRule[]
|
|
147
|
+
/** The downlink throttle rules. */
|
|
148
|
+
down?: ThrottleRule | ThrottleRule[]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function applyRules(
|
|
152
|
+
config: ThrottleConfig,
|
|
153
|
+
direction: 'up' | 'down',
|
|
154
|
+
device: string,
|
|
155
|
+
index: number,
|
|
156
|
+
protocol?: 'udp' | 'tcp',
|
|
157
|
+
match?: string,
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
let rules = config[direction]
|
|
160
|
+
if (!rules) return
|
|
161
|
+
log.info(
|
|
162
|
+
`applyRules device=${device} index=${index} protocol=${protocol} match=${match} ${JSON.stringify(
|
|
163
|
+
rules,
|
|
164
|
+
)}`,
|
|
165
|
+
)
|
|
166
|
+
if (!Array.isArray(rules)) {
|
|
167
|
+
rules = [rules]
|
|
168
|
+
}
|
|
169
|
+
rules.sort((a, b) => {
|
|
170
|
+
return (a.at || 0) - (b.at || 0)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
for (const [i, rule] of rules.entries()) {
|
|
174
|
+
const {
|
|
175
|
+
rate,
|
|
176
|
+
delay,
|
|
177
|
+
delayJitter,
|
|
178
|
+
delayJitterCorrelation,
|
|
179
|
+
delayDistribution,
|
|
180
|
+
reorder,
|
|
181
|
+
reorderCorrelation,
|
|
182
|
+
reorderGap,
|
|
183
|
+
loss,
|
|
184
|
+
lossBurst,
|
|
185
|
+
queue,
|
|
186
|
+
at,
|
|
187
|
+
} = rule
|
|
188
|
+
const limit = queue ?? calculateBufferedPackets(rate || 0, delay || 0)
|
|
189
|
+
const mark = index + 1
|
|
190
|
+
const handle = index + 2
|
|
191
|
+
|
|
192
|
+
if (i === 0) {
|
|
193
|
+
const matches = [`'meta(nf_mark eq ${mark})'`]
|
|
194
|
+
if (protocol === 'udp') {
|
|
195
|
+
matches.push("'cmp(u8 at 9 layer network eq 0x11)'")
|
|
196
|
+
} else if (protocol === 'tcp') {
|
|
197
|
+
matches.push("'cmp(u8 at 9 layer network eq 0x6)'")
|
|
198
|
+
}
|
|
199
|
+
if (match) {
|
|
200
|
+
matches.push(match)
|
|
201
|
+
}
|
|
202
|
+
const cmd = `\
|
|
203
|
+
set -e;
|
|
204
|
+
|
|
205
|
+
sudo -n tc class add dev ${device} parent 1: classid 1:${handle} htb rate 1Gbit ceil 1Gbit;
|
|
206
|
+
|
|
207
|
+
sudo -n tc qdisc add dev ${device} \
|
|
208
|
+
parent 1:${handle} \
|
|
209
|
+
handle ${handle}: \
|
|
210
|
+
netem; \
|
|
211
|
+
|
|
212
|
+
sudo -n tc filter add dev ${device} \
|
|
213
|
+
parent 1: \
|
|
214
|
+
protocol ip \
|
|
215
|
+
basic match ${matches.join(' and ')} \
|
|
216
|
+
flowid 1:${handle};
|
|
217
|
+
`
|
|
218
|
+
try {
|
|
219
|
+
await runShellCommand(cmd, true)
|
|
220
|
+
} catch (err) {
|
|
221
|
+
log.error(`error running "${cmd}": ${(err as Error).stack}`)
|
|
222
|
+
throw err
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const timeoutId = setTimeout(
|
|
227
|
+
async () => {
|
|
228
|
+
let desc = ''
|
|
229
|
+
|
|
230
|
+
if (rate && rate > 0) {
|
|
231
|
+
desc += ` rate ${rate}kbit`
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (limit && limit > 0) {
|
|
235
|
+
desc += ` limit ${limit}`
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (delay && delay > 0) {
|
|
239
|
+
desc += ` delay ${delay}ms`
|
|
240
|
+
if (delayJitter && delayJitter > 0) {
|
|
241
|
+
desc += ` ${delayJitter}ms`
|
|
242
|
+
if (delayJitterCorrelation && delayJitterCorrelation > 0) {
|
|
243
|
+
desc += ` ${delayJitterCorrelation}`
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (delayDistribution) {
|
|
247
|
+
desc += ` distribution ${delayDistribution}`
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (loss && loss > 0) {
|
|
252
|
+
if (lossBurst && lossBurst > 0) {
|
|
253
|
+
const p = (100 * loss) / (lossBurst * (100 - loss))
|
|
254
|
+
const r = 100 / lossBurst
|
|
255
|
+
desc += ` loss gemodel ${toPrecision(p, 2)} ${toPrecision(r, 2)}`
|
|
256
|
+
} else {
|
|
257
|
+
desc += ` loss ${toPrecision(loss, 2)}%`
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (reorder && reorder > 0) {
|
|
262
|
+
desc += ` reorder ${toPrecision(reorder, 2)}%`
|
|
263
|
+
if (reorderCorrelation && reorderCorrelation > 0) {
|
|
264
|
+
desc += ` ${toPrecision(reorderCorrelation, 2)}`
|
|
265
|
+
}
|
|
266
|
+
if (reorderGap && reorderGap > 0) {
|
|
267
|
+
desc += ` gap ${reorderGap}`
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
log.info(`applying rules on ${device} (${mark}): ${desc}`)
|
|
272
|
+
const cmd = `\
|
|
273
|
+
sudo -n tc qdisc change dev ${device} \
|
|
274
|
+
parent 1:${handle} \
|
|
275
|
+
handle ${handle}: \
|
|
276
|
+
netem ${desc}`
|
|
277
|
+
try {
|
|
278
|
+
ruleTimeouts.delete(timeoutId)
|
|
279
|
+
|
|
280
|
+
await runShellCommand(cmd)
|
|
281
|
+
|
|
282
|
+
throttleCurrentValues[direction].set(index, {
|
|
283
|
+
rate: rate ? 1000 * rate : undefined,
|
|
284
|
+
delay: delay || undefined,
|
|
285
|
+
loss: loss || undefined,
|
|
286
|
+
queue: limit || undefined,
|
|
287
|
+
})
|
|
288
|
+
} catch (err) {
|
|
289
|
+
log.error(`error running "${cmd}": ${(err as Error).stack}`)
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
(at || 0) * 1000,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
ruleTimeouts.add(timeoutId)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function start(): Promise<void> {
|
|
300
|
+
if (!throttleConfig || !throttleConfig.length) return
|
|
301
|
+
|
|
302
|
+
let device = throttleConfig[0].device
|
|
303
|
+
if (device) {
|
|
304
|
+
try {
|
|
305
|
+
await checkNetworkInterface(device)
|
|
306
|
+
} catch (_err) {
|
|
307
|
+
log.warn(`Network interface ${device} not found, using default.`)
|
|
308
|
+
device = ''
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!device) {
|
|
312
|
+
device = await getDefaultNetworkInterface()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await runShellCommand(
|
|
316
|
+
`\
|
|
317
|
+
set -e;
|
|
318
|
+
|
|
319
|
+
sudo -n modprobe ifb || true;
|
|
320
|
+
sudo -n ip link add ifb0 type ifb || true;
|
|
321
|
+
sudo -n ip link set dev ifb0 up;
|
|
322
|
+
|
|
323
|
+
sudo -n tc qdisc add dev ${device} root handle 1: htb default 1;
|
|
324
|
+
sudo -n tc class add dev ${device} parent 1: classid 1:1 htb rate 1Gbit ceil 1Gbit;
|
|
325
|
+
|
|
326
|
+
sudo -n tc qdisc add dev ifb0 root handle 1: htb default 1;
|
|
327
|
+
sudo -n tc class add dev ifb0 parent 1: classid 1:1 htb rate 1Gbit ceil 1Gbit;
|
|
328
|
+
|
|
329
|
+
sudo -n tc qdisc add dev ${device} ingress handle ffff: || true;
|
|
330
|
+
sudo -n tc filter add dev ${device} \
|
|
331
|
+
parent ffff: \
|
|
332
|
+
protocol ip \
|
|
333
|
+
u32 \
|
|
334
|
+
match u32 0 0 \
|
|
335
|
+
action connmark \
|
|
336
|
+
action mirred egress \
|
|
337
|
+
redirect dev ifb0 \
|
|
338
|
+
flowid 1:1;
|
|
339
|
+
`,
|
|
340
|
+
true,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
let index = 0
|
|
344
|
+
for (const config of throttleConfig) {
|
|
345
|
+
if (config.up) {
|
|
346
|
+
await applyRules(
|
|
347
|
+
config,
|
|
348
|
+
'up',
|
|
349
|
+
device,
|
|
350
|
+
index,
|
|
351
|
+
config.protocol,
|
|
352
|
+
config.match,
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
if (config.down) {
|
|
356
|
+
await applyRules(
|
|
357
|
+
config,
|
|
358
|
+
'down',
|
|
359
|
+
'ifb0',
|
|
360
|
+
index,
|
|
361
|
+
config.protocol,
|
|
362
|
+
config.match,
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
if (config.capture) {
|
|
366
|
+
captureStops.set(
|
|
367
|
+
index,
|
|
368
|
+
capturePackets(index, config.capture, config.protocol),
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
index++
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Starts a network throttle configuration
|
|
377
|
+
* @param config A JSON5 configuration parsed as {@link ThrottleConfig}.
|
|
378
|
+
*/
|
|
379
|
+
export async function startThrottle(config: string): Promise<void> {
|
|
380
|
+
if (os.platform() !== 'linux') {
|
|
381
|
+
throw new Error('Throttle option is only supported on Linux')
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
throttleConfig = JSON5.parse(config) as ThrottleConfig[]
|
|
385
|
+
log.debug('Starting throttle with config:', throttleConfig)
|
|
386
|
+
await cleanup()
|
|
387
|
+
await start()
|
|
388
|
+
} catch (err) {
|
|
389
|
+
log.error(`startThrottle "${config}" error: ${(err as Error).stack}`)
|
|
390
|
+
await stopThrottle()
|
|
391
|
+
throw err
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Stops the network throttle.
|
|
397
|
+
*/
|
|
398
|
+
export async function stopThrottle(): Promise<void> {
|
|
399
|
+
if (os.platform() !== 'linux') {
|
|
400
|
+
throw new Error('Throttle option is only supported on Linux')
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
log.debug('Stopping throttle')
|
|
404
|
+
await cleanup()
|
|
405
|
+
log.debug('Stopping throttle done')
|
|
406
|
+
throttleConfig = null
|
|
407
|
+
} catch (err) {
|
|
408
|
+
log.error(`Stop throttle error: ${(err as Error).stack}`)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function getSessionThrottleIndex(sessionId: number): number {
|
|
413
|
+
if (!throttleConfig) return -1
|
|
414
|
+
|
|
415
|
+
for (const [index, config] of throttleConfig.entries()) {
|
|
416
|
+
if (config.sessions === undefined || config.sessions === '') {
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
if (config.sessions.includes('-')) {
|
|
421
|
+
const [start, end] = config.sessions.split('-').map(Number)
|
|
422
|
+
if (sessionId >= start && sessionId <= end) {
|
|
423
|
+
return index
|
|
424
|
+
}
|
|
425
|
+
} else if (config.sessions.includes(',')) {
|
|
426
|
+
const sessions = config.sessions.split(',').map(Number)
|
|
427
|
+
if (sessions.includes(sessionId)) {
|
|
428
|
+
return index
|
|
429
|
+
}
|
|
430
|
+
} else if (sessionId === Number(config.sessions)) {
|
|
431
|
+
return index
|
|
432
|
+
}
|
|
433
|
+
} catch (err) {
|
|
434
|
+
log.error(
|
|
435
|
+
`getSessionThrottleId sessionId=${sessionId} error: ${(err as Error).stack}`,
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return -1
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function getSessionThrottleValues(
|
|
444
|
+
index: number,
|
|
445
|
+
direction: 'up' | 'down',
|
|
446
|
+
): {
|
|
447
|
+
rate?: number
|
|
448
|
+
delay?: number
|
|
449
|
+
loss?: number
|
|
450
|
+
queue?: number
|
|
451
|
+
} {
|
|
452
|
+
if (index < 0) {
|
|
453
|
+
return {}
|
|
454
|
+
}
|
|
455
|
+
return throttleCurrentValues[direction].get(index) || {}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export async function throttleLauncher(
|
|
459
|
+
executablePath: string,
|
|
460
|
+
index: number,
|
|
461
|
+
): Promise<string> {
|
|
462
|
+
log.debug(`throttleLauncher executablePath=${executablePath} index=${index}`)
|
|
463
|
+
if (!throttleConfig || index < 0) {
|
|
464
|
+
return executablePath
|
|
465
|
+
}
|
|
466
|
+
const config = throttleConfig[index]
|
|
467
|
+
const mark = index + 1
|
|
468
|
+
const launcherPath = `/tmp/throttler-launcher-${index}`
|
|
469
|
+
const group = `throttler${index}`
|
|
470
|
+
const filters = `${config.protocol ? `-p ${config.protocol}` : ''}\
|
|
471
|
+
${config.skipSourcePorts ? ` -m multiport ! --sports ${config.skipSourcePorts}` : ''}\
|
|
472
|
+
${config.skipDestinationPorts ? ` -m multiport ! --dports ${config.skipDestinationPorts}` : ''}\
|
|
473
|
+
${config.filter ? ` ${config.filter}` : ''}`
|
|
474
|
+
await fs.promises.writeFile(
|
|
475
|
+
launcherPath,
|
|
476
|
+
`#!/bin/bash
|
|
477
|
+
getent group ${group} >/dev/null || sudo -n addgroup --system ${group}
|
|
478
|
+
sudo -n adduser $USER ${group} --quiet
|
|
479
|
+
|
|
480
|
+
rule=$(sudo -n iptables -t mangle -L OUTPUT --line-numbers | grep "owner GID match ${group}" | awk '{print $1}')
|
|
481
|
+
if [ -n "$rule" ]; then
|
|
482
|
+
sudo -n iptables -t mangle -R OUTPUT \${rule} ${filters} -m owner --gid-owner ${group} -j MARK --set-mark ${mark}
|
|
483
|
+
else
|
|
484
|
+
sudo -n iptables -t mangle -I OUTPUT 1 ${filters} -m owner --gid-owner ${group} -j MARK --set-mark ${mark}
|
|
485
|
+
fi
|
|
486
|
+
|
|
487
|
+
sudo -n iptables -t mangle -L PREROUTING | grep -q "CONNMARK restore" || sudo -n iptables -t mangle -I PREROUTING 1 -j CONNMARK --restore-mark
|
|
488
|
+
sudo -n iptables -t mangle -L POSTROUTING | grep -q "CONNMARK save" || sudo -n iptables -t mangle -I POSTROUTING 1 -j CONNMARK --save-mark
|
|
489
|
+
|
|
490
|
+
function stop() {
|
|
491
|
+
echo "Stopping throttler"
|
|
492
|
+
}
|
|
493
|
+
trap stop SIGINT SIGTERM
|
|
494
|
+
|
|
495
|
+
echo "running: ${executablePath} $@"
|
|
496
|
+
exec newgrp ${group} <<EOF
|
|
497
|
+
${executablePath} $@
|
|
498
|
+
EOF`,
|
|
499
|
+
)
|
|
500
|
+
await fs.promises.chmod(launcherPath, 0o755)
|
|
501
|
+
return launcherPath
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function cleanupRules(): Promise<void> {
|
|
505
|
+
if (!throttleConfig?.length) return
|
|
506
|
+
log.debug(`cleanupRules (${throttleConfig.length})`)
|
|
507
|
+
try {
|
|
508
|
+
await runShellCommand(`\
|
|
509
|
+
for i in $(seq 0 ${throttleConfig.length}); do
|
|
510
|
+
rule=$(sudo -n iptables -t mangle -L OUTPUT --line-numbers | grep "owner GID match throttler\${i}" | awk '{print $1}');
|
|
511
|
+
if [ -n "$rule" ]; then
|
|
512
|
+
sudo -n iptables -t mangle -D OUTPUT \${rule};
|
|
513
|
+
fi;
|
|
514
|
+
done;`)
|
|
515
|
+
} catch (err) {
|
|
516
|
+
log.error(`cleanupRules error: ${(err as Error).stack}`)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function capturePackets(
|
|
521
|
+
index: number,
|
|
522
|
+
filePath: string,
|
|
523
|
+
protocol?: string,
|
|
524
|
+
): () => Promise<void> {
|
|
525
|
+
const mark = index + 1
|
|
526
|
+
log.info(`Starting capture ${filePath}`)
|
|
527
|
+
const cmd = `#!/bin/bash
|
|
528
|
+
sudo -n iptables -L INPUT | grep -q "nflog-group ${mark}" || sudo -n iptables -A INPUT ${protocol ? `-p ${protocol}` : ''} -m connmark --mark ${mark} -j NFLOG --nflog-group ${mark}
|
|
529
|
+
sudo -n iptables -L OUTPUT | grep -q "nflog-group ${mark}" || sudo -n iptables -A OUTPUT ${protocol ? `-p ${protocol}` : ''} -m connmark --mark ${mark} -j NFLOG --nflog-group ${mark}
|
|
530
|
+
exec dumpcap -q -i nflog:${mark} -w ${filePath}
|
|
531
|
+
`
|
|
532
|
+
const proc = spawn(cmd, {
|
|
533
|
+
shell: true,
|
|
534
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
535
|
+
detached: true,
|
|
536
|
+
})
|
|
537
|
+
let stderr = ''
|
|
538
|
+
proc.stderr.on('data', data => {
|
|
539
|
+
stderr += data
|
|
540
|
+
})
|
|
541
|
+
proc.on('error', err => {
|
|
542
|
+
log.error(`Error running command capturePackets ${err}: ${stderr}`)
|
|
543
|
+
})
|
|
544
|
+
proc.once('exit', code => {
|
|
545
|
+
if (code) {
|
|
546
|
+
log.error(`capturePackets exited with code ${code}: ${stderr}`)
|
|
547
|
+
} else {
|
|
548
|
+
log.info(`capturePackets exited`)
|
|
549
|
+
}
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
const stop = async () => {
|
|
553
|
+
log.info(`Stopping capture ${filePath}`)
|
|
554
|
+
proc.kill('SIGINT')
|
|
555
|
+
await runShellCommand(`#!/bin/bash
|
|
556
|
+
sudo -n iptables -D INPUT ${protocol ? `-p ${protocol}` : ''} -m connmark --mark ${mark} -j NFLOG --nflog-group ${mark}
|
|
557
|
+
sudo -n iptables -D OUTPUT ${protocol ? `-p ${protocol}` : ''} -m connmark --mark ${mark} -j NFLOG --nflog-group ${mark}
|
|
558
|
+
`)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return stop
|
|
562
|
+
}
|