@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.
@@ -0,0 +1,385 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.startThrottle = startThrottle;
7
+ exports.stopThrottle = stopThrottle;
8
+ exports.getSessionThrottleIndex = getSessionThrottleIndex;
9
+ exports.getSessionThrottleValues = getSessionThrottleValues;
10
+ exports.throttleLauncher = throttleLauncher;
11
+ const child_process_1 = require("child_process");
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const json5_1 = __importDefault(require("json5"));
14
+ const os_1 = __importDefault(require("os"));
15
+ const utils_1 = require("./utils");
16
+ const log = (0, utils_1.logger)('throttler:throttle');
17
+ let throttleConfig = null;
18
+ const ruleTimeouts = new Set();
19
+ const captureStops = new Map();
20
+ const throttleCurrentValues = {
21
+ up: new Map(),
22
+ down: new Map(),
23
+ };
24
+ async function cleanup() {
25
+ await Promise.allSettled([...captureStops.values()].map(stop => stop()));
26
+ captureStops.clear();
27
+ ruleTimeouts.forEach(timeoutId => clearTimeout(timeoutId));
28
+ ruleTimeouts.clear();
29
+ throttleCurrentValues.up.clear();
30
+ throttleCurrentValues.down.clear();
31
+ let device = throttleConfig?.length ? throttleConfig[0].device : '';
32
+ if (!device) {
33
+ device = await (0, utils_1.getDefaultNetworkInterface)();
34
+ }
35
+ await (0, utils_1.runShellCommand)(`\
36
+ sudo -n tc qdisc del dev ${device} root || true;
37
+ sudo -n tc class del dev ${device} || true;
38
+ sudo -n tc filter del dev ${device} || true;
39
+ sudo -n tc qdisc del dev ${device} ingress || true;
40
+
41
+ sudo -n tc qdisc del dev ifb0 root || true;
42
+ sudo -n tc class del dev ifb0 root || true;
43
+ sudo -n tc filter del dev ifb0 root || true;
44
+ `);
45
+ await cleanupRules();
46
+ }
47
+ function calculateBufferedPackets(rate, delay, mtu = 1500) {
48
+ // https://lists.linuxfoundation.org/pipermail/netem/2007-March/001094.html
49
+ return Math.ceil((((1.5 * rate * 1000) / 8) * (delay / 1000)) / mtu);
50
+ }
51
+ async function applyRules(config, direction, device, index, protocol, match) {
52
+ let rules = config[direction];
53
+ if (!rules)
54
+ return;
55
+ log.info(`applyRules device=${device} index=${index} protocol=${protocol} match=${match} ${JSON.stringify(rules)}`);
56
+ if (!Array.isArray(rules)) {
57
+ rules = [rules];
58
+ }
59
+ rules.sort((a, b) => {
60
+ return (a.at || 0) - (b.at || 0);
61
+ });
62
+ for (const [i, rule] of rules.entries()) {
63
+ const { rate, delay, delayJitter, delayJitterCorrelation, delayDistribution, reorder, reorderCorrelation, reorderGap, loss, lossBurst, queue, at, } = rule;
64
+ const limit = queue ?? calculateBufferedPackets(rate || 0, delay || 0);
65
+ const mark = index + 1;
66
+ const handle = index + 2;
67
+ if (i === 0) {
68
+ const matches = [`'meta(nf_mark eq ${mark})'`];
69
+ if (protocol === 'udp') {
70
+ matches.push("'cmp(u8 at 9 layer network eq 0x11)'");
71
+ }
72
+ else if (protocol === 'tcp') {
73
+ matches.push("'cmp(u8 at 9 layer network eq 0x6)'");
74
+ }
75
+ if (match) {
76
+ matches.push(match);
77
+ }
78
+ const cmd = `\
79
+ set -e;
80
+
81
+ sudo -n tc class add dev ${device} parent 1: classid 1:${handle} htb rate 1Gbit ceil 1Gbit;
82
+
83
+ sudo -n tc qdisc add dev ${device} \
84
+ parent 1:${handle} \
85
+ handle ${handle}: \
86
+ netem; \
87
+
88
+ sudo -n tc filter add dev ${device} \
89
+ parent 1: \
90
+ protocol ip \
91
+ basic match ${matches.join(' and ')} \
92
+ flowid 1:${handle};
93
+ `;
94
+ try {
95
+ await (0, utils_1.runShellCommand)(cmd, true);
96
+ }
97
+ catch (err) {
98
+ log.error(`error running "${cmd}": ${err.stack}`);
99
+ throw err;
100
+ }
101
+ }
102
+ const timeoutId = setTimeout(async () => {
103
+ let desc = '';
104
+ if (rate && rate > 0) {
105
+ desc += ` rate ${rate}kbit`;
106
+ }
107
+ if (limit && limit > 0) {
108
+ desc += ` limit ${limit}`;
109
+ }
110
+ if (delay && delay > 0) {
111
+ desc += ` delay ${delay}ms`;
112
+ if (delayJitter && delayJitter > 0) {
113
+ desc += ` ${delayJitter}ms`;
114
+ if (delayJitterCorrelation && delayJitterCorrelation > 0) {
115
+ desc += ` ${delayJitterCorrelation}`;
116
+ }
117
+ }
118
+ if (delayDistribution) {
119
+ desc += ` distribution ${delayDistribution}`;
120
+ }
121
+ }
122
+ if (loss && loss > 0) {
123
+ if (lossBurst && lossBurst > 0) {
124
+ const p = (100 * loss) / (lossBurst * (100 - loss));
125
+ const r = 100 / lossBurst;
126
+ desc += ` loss gemodel ${(0, utils_1.toPrecision)(p, 2)} ${(0, utils_1.toPrecision)(r, 2)}`;
127
+ }
128
+ else {
129
+ desc += ` loss ${(0, utils_1.toPrecision)(loss, 2)}%`;
130
+ }
131
+ }
132
+ if (reorder && reorder > 0) {
133
+ desc += ` reorder ${(0, utils_1.toPrecision)(reorder, 2)}%`;
134
+ if (reorderCorrelation && reorderCorrelation > 0) {
135
+ desc += ` ${(0, utils_1.toPrecision)(reorderCorrelation, 2)}`;
136
+ }
137
+ if (reorderGap && reorderGap > 0) {
138
+ desc += ` gap ${reorderGap}`;
139
+ }
140
+ }
141
+ log.info(`applying rules on ${device} (${mark}): ${desc}`);
142
+ const cmd = `\
143
+ sudo -n tc qdisc change dev ${device} \
144
+ parent 1:${handle} \
145
+ handle ${handle}: \
146
+ netem ${desc}`;
147
+ try {
148
+ ruleTimeouts.delete(timeoutId);
149
+ await (0, utils_1.runShellCommand)(cmd);
150
+ throttleCurrentValues[direction].set(index, {
151
+ rate: rate ? 1000 * rate : undefined,
152
+ delay: delay || undefined,
153
+ loss: loss || undefined,
154
+ queue: limit || undefined,
155
+ });
156
+ }
157
+ catch (err) {
158
+ log.error(`error running "${cmd}": ${err.stack}`);
159
+ }
160
+ }, (at || 0) * 1000);
161
+ ruleTimeouts.add(timeoutId);
162
+ }
163
+ }
164
+ async function start() {
165
+ if (!throttleConfig || !throttleConfig.length)
166
+ return;
167
+ let device = throttleConfig[0].device;
168
+ if (device) {
169
+ try {
170
+ await (0, utils_1.checkNetworkInterface)(device);
171
+ }
172
+ catch (_err) {
173
+ log.warn(`Network interface ${device} not found, using default.`);
174
+ device = '';
175
+ }
176
+ }
177
+ if (!device) {
178
+ device = await (0, utils_1.getDefaultNetworkInterface)();
179
+ }
180
+ await (0, utils_1.runShellCommand)(`\
181
+ set -e;
182
+
183
+ sudo -n modprobe ifb || true;
184
+ sudo -n ip link add ifb0 type ifb || true;
185
+ sudo -n ip link set dev ifb0 up;
186
+
187
+ sudo -n tc qdisc add dev ${device} root handle 1: htb default 1;
188
+ sudo -n tc class add dev ${device} parent 1: classid 1:1 htb rate 1Gbit ceil 1Gbit;
189
+
190
+ sudo -n tc qdisc add dev ifb0 root handle 1: htb default 1;
191
+ sudo -n tc class add dev ifb0 parent 1: classid 1:1 htb rate 1Gbit ceil 1Gbit;
192
+
193
+ sudo -n tc qdisc add dev ${device} ingress handle ffff: || true;
194
+ sudo -n tc filter add dev ${device} \
195
+ parent ffff: \
196
+ protocol ip \
197
+ u32 \
198
+ match u32 0 0 \
199
+ action connmark \
200
+ action mirred egress \
201
+ redirect dev ifb0 \
202
+ flowid 1:1;
203
+ `, true);
204
+ let index = 0;
205
+ for (const config of throttleConfig) {
206
+ if (config.up) {
207
+ await applyRules(config, 'up', device, index, config.protocol, config.match);
208
+ }
209
+ if (config.down) {
210
+ await applyRules(config, 'down', 'ifb0', index, config.protocol, config.match);
211
+ }
212
+ if (config.capture) {
213
+ captureStops.set(index, capturePackets(index, config.capture, config.protocol));
214
+ }
215
+ index++;
216
+ }
217
+ }
218
+ /**
219
+ * Starts a network throttle configuration
220
+ * @param config A JSON5 configuration parsed as {@link ThrottleConfig}.
221
+ */
222
+ async function startThrottle(config) {
223
+ if (os_1.default.platform() !== 'linux') {
224
+ throw new Error('Throttle option is only supported on Linux');
225
+ }
226
+ try {
227
+ throttleConfig = json5_1.default.parse(config);
228
+ log.debug('Starting throttle with config:', throttleConfig);
229
+ await cleanup();
230
+ await start();
231
+ }
232
+ catch (err) {
233
+ log.error(`startThrottle "${config}" error: ${err.stack}`);
234
+ await stopThrottle();
235
+ throw err;
236
+ }
237
+ }
238
+ /**
239
+ * Stops the network throttle.
240
+ */
241
+ async function stopThrottle() {
242
+ if (os_1.default.platform() !== 'linux') {
243
+ throw new Error('Throttle option is only supported on Linux');
244
+ }
245
+ try {
246
+ log.debug('Stopping throttle');
247
+ await cleanup();
248
+ log.debug('Stopping throttle done');
249
+ throttleConfig = null;
250
+ }
251
+ catch (err) {
252
+ log.error(`Stop throttle error: ${err.stack}`);
253
+ }
254
+ }
255
+ function getSessionThrottleIndex(sessionId) {
256
+ if (!throttleConfig)
257
+ return -1;
258
+ for (const [index, config] of throttleConfig.entries()) {
259
+ if (config.sessions === undefined || config.sessions === '') {
260
+ continue;
261
+ }
262
+ try {
263
+ if (config.sessions.includes('-')) {
264
+ const [start, end] = config.sessions.split('-').map(Number);
265
+ if (sessionId >= start && sessionId <= end) {
266
+ return index;
267
+ }
268
+ }
269
+ else if (config.sessions.includes(',')) {
270
+ const sessions = config.sessions.split(',').map(Number);
271
+ if (sessions.includes(sessionId)) {
272
+ return index;
273
+ }
274
+ }
275
+ else if (sessionId === Number(config.sessions)) {
276
+ return index;
277
+ }
278
+ }
279
+ catch (err) {
280
+ log.error(`getSessionThrottleId sessionId=${sessionId} error: ${err.stack}`);
281
+ }
282
+ }
283
+ return -1;
284
+ }
285
+ function getSessionThrottleValues(index, direction) {
286
+ if (index < 0) {
287
+ return {};
288
+ }
289
+ return throttleCurrentValues[direction].get(index) || {};
290
+ }
291
+ async function throttleLauncher(executablePath, index) {
292
+ log.debug(`throttleLauncher executablePath=${executablePath} index=${index}`);
293
+ if (!throttleConfig || index < 0) {
294
+ return executablePath;
295
+ }
296
+ const config = throttleConfig[index];
297
+ const mark = index + 1;
298
+ const launcherPath = `/tmp/throttler-launcher-${index}`;
299
+ const group = `throttler${index}`;
300
+ const filters = `${config.protocol ? `-p ${config.protocol}` : ''}\
301
+ ${config.skipSourcePorts ? ` -m multiport ! --sports ${config.skipSourcePorts}` : ''}\
302
+ ${config.skipDestinationPorts ? ` -m multiport ! --dports ${config.skipDestinationPorts}` : ''}\
303
+ ${config.filter ? ` ${config.filter}` : ''}`;
304
+ await fs_1.default.promises.writeFile(launcherPath, `#!/bin/bash
305
+ getent group ${group} >/dev/null || sudo -n addgroup --system ${group}
306
+ sudo -n adduser $USER ${group} --quiet
307
+
308
+ rule=$(sudo -n iptables -t mangle -L OUTPUT --line-numbers | grep "owner GID match ${group}" | awk '{print $1}')
309
+ if [ -n "$rule" ]; then
310
+ sudo -n iptables -t mangle -R OUTPUT \${rule} ${filters} -m owner --gid-owner ${group} -j MARK --set-mark ${mark}
311
+ else
312
+ sudo -n iptables -t mangle -I OUTPUT 1 ${filters} -m owner --gid-owner ${group} -j MARK --set-mark ${mark}
313
+ fi
314
+
315
+ sudo -n iptables -t mangle -L PREROUTING | grep -q "CONNMARK restore" || sudo -n iptables -t mangle -I PREROUTING 1 -j CONNMARK --restore-mark
316
+ sudo -n iptables -t mangle -L POSTROUTING | grep -q "CONNMARK save" || sudo -n iptables -t mangle -I POSTROUTING 1 -j CONNMARK --save-mark
317
+
318
+ function stop() {
319
+ echo "Stopping throttler"
320
+ }
321
+ trap stop SIGINT SIGTERM
322
+
323
+ echo "running: ${executablePath} $@"
324
+ exec newgrp ${group} <<EOF
325
+ ${executablePath} $@
326
+ EOF`);
327
+ await fs_1.default.promises.chmod(launcherPath, 0o755);
328
+ return launcherPath;
329
+ }
330
+ async function cleanupRules() {
331
+ if (!throttleConfig?.length)
332
+ return;
333
+ log.debug(`cleanupRules (${throttleConfig.length})`);
334
+ try {
335
+ await (0, utils_1.runShellCommand)(`\
336
+ for i in $(seq 0 ${throttleConfig.length}); do
337
+ rule=$(sudo -n iptables -t mangle -L OUTPUT --line-numbers | grep "owner GID match throttler\${i}" | awk '{print $1}');
338
+ if [ -n "$rule" ]; then
339
+ sudo -n iptables -t mangle -D OUTPUT \${rule};
340
+ fi;
341
+ done;`);
342
+ }
343
+ catch (err) {
344
+ log.error(`cleanupRules error: ${err.stack}`);
345
+ }
346
+ }
347
+ function capturePackets(index, filePath, protocol) {
348
+ const mark = index + 1;
349
+ log.info(`Starting capture ${filePath}`);
350
+ const cmd = `#!/bin/bash
351
+ 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}
352
+ 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}
353
+ exec dumpcap -q -i nflog:${mark} -w ${filePath}
354
+ `;
355
+ const proc = (0, child_process_1.spawn)(cmd, {
356
+ shell: true,
357
+ stdio: ['ignore', 'ignore', 'pipe'],
358
+ detached: true,
359
+ });
360
+ let stderr = '';
361
+ proc.stderr.on('data', data => {
362
+ stderr += data;
363
+ });
364
+ proc.on('error', err => {
365
+ log.error(`Error running command capturePackets ${err}: ${stderr}`);
366
+ });
367
+ proc.once('exit', code => {
368
+ if (code) {
369
+ log.error(`capturePackets exited with code ${code}: ${stderr}`);
370
+ }
371
+ else {
372
+ log.info(`capturePackets exited`);
373
+ }
374
+ });
375
+ const stop = async () => {
376
+ log.info(`Stopping capture ${filePath}`);
377
+ proc.kill('SIGINT');
378
+ await (0, utils_1.runShellCommand)(`#!/bin/bash
379
+ sudo -n iptables -D INPUT ${protocol ? `-p ${protocol}` : ''} -m connmark --mark ${mark} -j NFLOG --nflog-group ${mark}
380
+ sudo -n iptables -D OUTPUT ${protocol ? `-p ${protocol}` : ''} -m connmark --mark ${mark} -j NFLOG --nflog-group ${mark}
381
+ `);
382
+ };
383
+ return stop;
384
+ }
385
+ //# sourceMappingURL=throttle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"throttle.js","sourceRoot":"","sources":["../../src/throttle.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'child_process'\nimport fs from 'fs'\nimport JSON5 from 'json5'\nimport os from 'os'\n\nimport {\n checkNetworkInterface,\n getDefaultNetworkInterface,\n logger,\n runShellCommand,\n toPrecision,\n} from './utils'\n\nconst log = logger('throttler:throttle')\n\nlet throttleConfig: ThrottleConfig[] | null = null\n\nconst ruleTimeouts = new Set<NodeJS.Timeout>()\n\nconst captureStops = new Map<number, () => Promise<void>>()\n\nconst throttleCurrentValues = {\n up: new Map<\n number,\n {\n rate?: number\n delay?: number\n delayJitter?: number\n delayJitterCorrelation?: number\n loss?: number\n lossBurst?: number\n queue?: number\n }\n >(),\n down: new Map<\n number,\n {\n rate?: number\n delay?: number\n delayJitterCorrelation?: number\n loss?: number\n lossBurst?: number\n queue?: number\n }\n >(),\n}\n\nasync function cleanup(): Promise<void> {\n await Promise.allSettled([...captureStops.values()].map(stop => stop()))\n captureStops.clear()\n ruleTimeouts.forEach(timeoutId => clearTimeout(timeoutId))\n ruleTimeouts.clear()\n throttleCurrentValues.up.clear()\n throttleCurrentValues.down.clear()\n let device = throttleConfig?.length ? throttleConfig[0].device : ''\n if (!device) {\n device = await getDefaultNetworkInterface()\n }\n await runShellCommand(`\\\nsudo -n tc qdisc del dev ${device} root || true;\nsudo -n tc class del dev ${device} || true;\nsudo -n tc filter del dev ${device} || true;\nsudo -n tc qdisc del dev ${device} ingress || true;\n\nsudo -n tc qdisc del dev ifb0 root || true;\nsudo -n tc class del dev ifb0 root || true;\nsudo -n tc filter del dev ifb0 root || true;\n`)\n await cleanupRules()\n}\n\nfunction calculateBufferedPackets(\n rate: number,\n delay: number,\n mtu = 1500,\n): number {\n // https://lists.linuxfoundation.org/pipermail/netem/2007-March/001094.html\n return Math.ceil((((1.5 * rate * 1000) / 8) * (delay / 1000)) / mtu)\n}\n\n/** The network throttle rules to be applied to uplink or downlink. */\nexport type ThrottleRule = {\n /** The available bandwidth (Kbps). */\n rate?: number\n /** The one-way delay (ms). */\n delay?: number\n /** The one-way delay jitter (ms). */\n delayJitter?: number\n /** The one-way delay jitter correlation. */\n delayJitterCorrelation?: number\n /** The delay distribution. */\n delayDistribution?: 'uniform' | 'normal' | 'pareto' | 'paretonormal'\n /** The packet reordering percentage. */\n reorder?: number\n /** The packet reordering correlation. */\n reorderCorrelation?: number\n /** The packet reordering gap. */\n reorderGap?: number\n /** The packet loss percentage. */\n loss?: number\n /** The packet loss burst. */\n lossBurst?: number\n /** The packet queue size. */\n queue?: number\n /** If set, the rule will be applied after the specified number of seconds. */\n at?: number\n}\n\n/**\n * The network throttling rules.\n * Specify multiple {@link ThrottleRule} with different `at` values to schedule\n * network bandwidth/delay fluctuations during the test run, e.g.:\n *\n * ```javascript\n * {\n device: \"eth0\",\n sessions: \"0-1\",\n protocol: \"udp\",\n down: [\n { rate: 1000000, delay: 50, loss: 0, queue: 5 },\n { rate: 200000, delay: 100, loss: 5, queue: 5, at: 60},\n ],\n up: { rate: 100000, delay: 50, queue: 5 },\n capture: 'capture.pcap',\n }\n * ```\n */\nexport type ThrottleConfig = {\n /** The network interface to throttle. If not specified, the default interface will be used. */\n device?: string\n /** The sessions to throttle. It could be a single index (\"0\"), a range (\"0-2\") or a comma-separated list (\"0,3,4\"). */\n sessions?: string\n /** The protocol to throttle. */\n protocol?: 'udp' | 'tcp'\n /** A comma-separated list of source ports that will not be throttled. */\n skipSourcePorts?: string\n /** A comma-separated list of destination ports that will not be throttled. */\n skipDestinationPorts?: string\n /** An additional IPTables packet filter rule. */\n filter?: string\n /** An additional TC match expression used to filter packets (https://man7.org/linux/man-pages/man8/tc-ematch.8.html). */\n match?: string\n /** If set, the packets matching the provided session and protocol will be captured at that file location. */\n capture?: string\n /** The uplink throttle rules. */\n up?: ThrottleRule | ThrottleRule[]\n /** The downlink throttle rules. */\n down?: ThrottleRule | ThrottleRule[]\n}\n\nasync function applyRules(\n config: ThrottleConfig,\n direction: 'up' | 'down',\n device: string,\n index: number,\n protocol?: 'udp' | 'tcp',\n match?: string,\n): Promise<void> {\n let rules = config[direction]\n if (!rules) return\n log.info(\n `applyRules device=${device} index=${index} protocol=${protocol} match=${match} ${JSON.stringify(\n rules,\n )}`,\n )\n if (!Array.isArray(rules)) {\n rules = [rules]\n }\n rules.sort((a, b) => {\n return (a.at || 0) - (b.at || 0)\n })\n\n for (const [i, rule] of rules.entries()) {\n const {\n rate,\n delay,\n delayJitter,\n delayJitterCorrelation,\n delayDistribution,\n reorder,\n reorderCorrelation,\n reorderGap,\n loss,\n lossBurst,\n queue,\n at,\n } = rule\n const limit = queue ?? calculateBufferedPackets(rate || 0, delay || 0)\n const mark = index + 1\n const handle = index + 2\n\n if (i === 0) {\n const matches = [`'meta(nf_mark eq ${mark})'`]\n if (protocol === 'udp') {\n matches.push(\"'cmp(u8 at 9 layer network eq 0x11)'\")\n } else if (protocol === 'tcp') {\n matches.push(\"'cmp(u8 at 9 layer network eq 0x6)'\")\n }\n if (match) {\n matches.push(match)\n }\n const cmd = `\\\nset -e;\n\nsudo -n tc class add dev ${device} parent 1: classid 1:${handle} htb rate 1Gbit ceil 1Gbit;\n\nsudo -n tc qdisc add dev ${device} \\\n parent 1:${handle} \\\n handle ${handle}: \\\n netem; \\\n\nsudo -n tc filter add dev ${device} \\\n parent 1: \\\n protocol ip \\\n basic match ${matches.join(' and ')} \\\n flowid 1:${handle};\n`\n try {\n await runShellCommand(cmd, true)\n } catch (err) {\n log.error(`error running \"${cmd}\": ${(err as Error).stack}`)\n throw err\n }\n }\n\n const timeoutId = setTimeout(\n async () => {\n let desc = ''\n\n if (rate && rate > 0) {\n desc += ` rate ${rate}kbit`\n }\n\n if (limit && limit > 0) {\n desc += ` limit ${limit}`\n }\n\n if (delay && delay > 0) {\n desc += ` delay ${delay}ms`\n if (delayJitter && delayJitter > 0) {\n desc += ` ${delayJitter}ms`\n if (delayJitterCorrelation && delayJitterCorrelation > 0) {\n desc += ` ${delayJitterCorrelation}`\n }\n }\n if (delayDistribution) {\n desc += ` distribution ${delayDistribution}`\n }\n }\n\n if (loss && loss > 0) {\n if (lossBurst && lossBurst > 0) {\n const p = (100 * loss) / (lossBurst * (100 - loss))\n const r = 100 / lossBurst\n desc += ` loss gemodel ${toPrecision(p, 2)} ${toPrecision(r, 2)}`\n } else {\n desc += ` loss ${toPrecision(loss, 2)}%`\n }\n }\n\n if (reorder && reorder > 0) {\n desc += ` reorder ${toPrecision(reorder, 2)}%`\n if (reorderCorrelation && reorderCorrelation > 0) {\n desc += ` ${toPrecision(reorderCorrelation, 2)}`\n }\n if (reorderGap && reorderGap > 0) {\n desc += ` gap ${reorderGap}`\n }\n }\n\n log.info(`applying rules on ${device} (${mark}): ${desc}`)\n const cmd = `\\\nsudo -n tc qdisc change dev ${device} \\\n parent 1:${handle} \\\n handle ${handle}: \\\n netem ${desc}`\n try {\n ruleTimeouts.delete(timeoutId)\n\n await runShellCommand(cmd)\n\n throttleCurrentValues[direction].set(index, {\n rate: rate ? 1000 * rate : undefined,\n delay: delay || undefined,\n loss: loss || undefined,\n queue: limit || undefined,\n })\n } catch (err) {\n log.error(`error running \"${cmd}\": ${(err as Error).stack}`)\n }\n },\n (at || 0) * 1000,\n )\n\n ruleTimeouts.add(timeoutId)\n }\n}\n\nasync function start(): Promise<void> {\n if (!throttleConfig || !throttleConfig.length) return\n\n let device = throttleConfig[0].device\n if (device) {\n try {\n await checkNetworkInterface(device)\n } catch (_err) {\n log.warn(`Network interface ${device} not found, using default.`)\n device = ''\n }\n }\n if (!device) {\n device = await getDefaultNetworkInterface()\n }\n\n await runShellCommand(\n `\\\nset -e;\n\nsudo -n modprobe ifb || true;\nsudo -n ip link add ifb0 type ifb || true;\nsudo -n ip link set dev ifb0 up;\n\nsudo -n tc qdisc add dev ${device} root handle 1: htb default 1;\nsudo -n tc class add dev ${device} parent 1: classid 1:1 htb rate 1Gbit ceil 1Gbit;\n\nsudo -n tc qdisc add dev ifb0 root handle 1: htb default 1;\nsudo -n tc class add dev ifb0 parent 1: classid 1:1 htb rate 1Gbit ceil 1Gbit;\n\nsudo -n tc qdisc add dev ${device} ingress handle ffff: || true;\nsudo -n tc filter add dev ${device} \\\n parent ffff: \\\n protocol ip \\\n u32 \\\n match u32 0 0 \\\n action connmark \\\n action mirred egress \\\n redirect dev ifb0 \\\n flowid 1:1;\n`,\n true,\n )\n\n let index = 0\n for (const config of throttleConfig) {\n if (config.up) {\n await applyRules(\n config,\n 'up',\n device,\n index,\n config.protocol,\n config.match,\n )\n }\n if (config.down) {\n await applyRules(\n config,\n 'down',\n 'ifb0',\n index,\n config.protocol,\n config.match,\n )\n }\n if (config.capture) {\n captureStops.set(\n index,\n capturePackets(index, config.capture, config.protocol),\n )\n }\n index++\n }\n}\n\n/**\n * Starts a network throttle configuration\n * @param config A JSON5 configuration parsed as {@link ThrottleConfig}.\n */\nexport async function startThrottle(config: string): Promise<void> {\n if (os.platform() !== 'linux') {\n throw new Error('Throttle option is only supported on Linux')\n }\n try {\n throttleConfig = JSON5.parse(config) as ThrottleConfig[]\n log.debug('Starting throttle with config:', throttleConfig)\n await cleanup()\n await start()\n } catch (err) {\n log.error(`startThrottle \"${config}\" error: ${(err as Error).stack}`)\n await stopThrottle()\n throw err\n }\n}\n\n/**\n * Stops the network throttle.\n */\nexport async function stopThrottle(): Promise<void> {\n if (os.platform() !== 'linux') {\n throw new Error('Throttle option is only supported on Linux')\n }\n try {\n log.debug('Stopping throttle')\n await cleanup()\n log.debug('Stopping throttle done')\n throttleConfig = null\n } catch (err) {\n log.error(`Stop throttle error: ${(err as Error).stack}`)\n }\n}\n\nexport function getSessionThrottleIndex(sessionId: number): number {\n if (!throttleConfig) return -1\n\n for (const [index, config] of throttleConfig.entries()) {\n if (config.sessions === undefined || config.sessions === '') {\n continue\n }\n try {\n if (config.sessions.includes('-')) {\n const [start, end] = config.sessions.split('-').map(Number)\n if (sessionId >= start && sessionId <= end) {\n return index\n }\n } else if (config.sessions.includes(',')) {\n const sessions = config.sessions.split(',').map(Number)\n if (sessions.includes(sessionId)) {\n return index\n }\n } else if (sessionId === Number(config.sessions)) {\n return index\n }\n } catch (err) {\n log.error(\n `getSessionThrottleId sessionId=${sessionId} error: ${(err as Error).stack}`,\n )\n }\n }\n\n return -1\n}\n\nexport function getSessionThrottleValues(\n index: number,\n direction: 'up' | 'down',\n): {\n rate?: number\n delay?: number\n loss?: number\n queue?: number\n} {\n if (index < 0) {\n return {}\n }\n return throttleCurrentValues[direction].get(index) || {}\n}\n\nexport async function throttleLauncher(\n executablePath: string,\n index: number,\n): Promise<string> {\n log.debug(`throttleLauncher executablePath=${executablePath} index=${index}`)\n if (!throttleConfig || index < 0) {\n return executablePath\n }\n const config = throttleConfig[index]\n const mark = index + 1\n const launcherPath = `/tmp/throttler-launcher-${index}`\n const group = `throttler${index}`\n const filters = `${config.protocol ? `-p ${config.protocol}` : ''}\\\n${config.skipSourcePorts ? ` -m multiport ! --sports ${config.skipSourcePorts}` : ''}\\\n${config.skipDestinationPorts ? ` -m multiport ! --dports ${config.skipDestinationPorts}` : ''}\\\n${config.filter ? ` ${config.filter}` : ''}`\n await fs.promises.writeFile(\n launcherPath,\n `#!/bin/bash\ngetent group ${group} >/dev/null || sudo -n addgroup --system ${group}\nsudo -n adduser $USER ${group} --quiet\n\nrule=$(sudo -n iptables -t mangle -L OUTPUT --line-numbers | grep \"owner GID match ${group}\" | awk '{print $1}')\nif [ -n \"$rule\" ]; then\n sudo -n iptables -t mangle -R OUTPUT \\${rule} ${filters} -m owner --gid-owner ${group} -j MARK --set-mark ${mark} \nelse\n sudo -n iptables -t mangle -I OUTPUT 1 ${filters} -m owner --gid-owner ${group} -j MARK --set-mark ${mark}\nfi\n\nsudo -n iptables -t mangle -L PREROUTING | grep -q \"CONNMARK restore\" || sudo -n iptables -t mangle -I PREROUTING 1 -j CONNMARK --restore-mark\nsudo -n iptables -t mangle -L POSTROUTING | grep -q \"CONNMARK save\" || sudo -n iptables -t mangle -I POSTROUTING 1 -j CONNMARK --save-mark\n\nfunction stop() {\n echo \"Stopping throttler\"\n}\ntrap stop SIGINT SIGTERM\n\necho \"running: ${executablePath} $@\"\nexec newgrp ${group} <<EOF\n${executablePath} $@\nEOF`,\n )\n await fs.promises.chmod(launcherPath, 0o755)\n return launcherPath\n}\n\nasync function cleanupRules(): Promise<void> {\n if (!throttleConfig?.length) return\n log.debug(`cleanupRules (${throttleConfig.length})`)\n try {\n await runShellCommand(`\\\nfor i in $(seq 0 ${throttleConfig.length}); do\n rule=$(sudo -n iptables -t mangle -L OUTPUT --line-numbers | grep \"owner GID match throttler\\${i}\" | awk '{print $1}');\n if [ -n \"$rule\" ]; then\n sudo -n iptables -t mangle -D OUTPUT \\${rule};\n fi;\ndone;`)\n } catch (err) {\n log.error(`cleanupRules error: ${(err as Error).stack}`)\n }\n}\n\nfunction capturePackets(\n index: number,\n filePath: string,\n protocol?: string,\n): () => Promise<void> {\n const mark = index + 1\n log.info(`Starting capture ${filePath}`)\n const cmd = `#!/bin/bash\nsudo -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}\nsudo -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}\nexec dumpcap -q -i nflog:${mark} -w ${filePath}\n`\n const proc = spawn(cmd, {\n shell: true,\n stdio: ['ignore', 'ignore', 'pipe'],\n detached: true,\n })\n let stderr = ''\n proc.stderr.on('data', data => {\n stderr += data\n })\n proc.on('error', err => {\n log.error(`Error running command capturePackets ${err}: ${stderr}`)\n })\n proc.once('exit', code => {\n if (code) {\n log.error(`capturePackets exited with code ${code}: ${stderr}`)\n } else {\n log.info(`capturePackets exited`)\n }\n })\n\n const stop = async () => {\n log.info(`Stopping capture ${filePath}`)\n proc.kill('SIGINT')\n await runShellCommand(`#!/bin/bash\nsudo -n iptables -D INPUT ${protocol ? `-p ${protocol}` : ''} -m connmark --mark ${mark} -j NFLOG --nflog-group ${mark}\nsudo -n iptables -D OUTPUT ${protocol ? `-p ${protocol}` : ''} -m connmark --mark ${mark} -j NFLOG --nflog-group ${mark}\n`)\n }\n\n return stop\n}\n"]}
@@ -0,0 +1,57 @@
1
+ export declare const Log: any;
2
+ type Logger = {
3
+ error: (...args: unknown[]) => void;
4
+ warn: (...args: unknown[]) => void;
5
+ info: (...args: unknown[]) => void;
6
+ debug: (...args: unknown[]) => void;
7
+ log: (...args: unknown[]) => void;
8
+ };
9
+ export declare function logger(name: string, options?: {}): Logger;
10
+ export declare class LoggerInterface {
11
+ name?: string;
12
+ private logInit;
13
+ debug(...args: unknown[]): void;
14
+ info(...args: unknown[]): void;
15
+ warn(...args: unknown[]): void;
16
+ error(...args: unknown[]): void;
17
+ log(...args: unknown[]): void;
18
+ }
19
+ /**
20
+ * Resolves the absolute path from the package installation directory.
21
+ * @param relativePath The relative path.
22
+ * @returns The absolute path.
23
+ */
24
+ export declare function resolvePackagePath(relativePath: string): string;
25
+ /**
26
+ * Format number to the specified precision.
27
+ * @param value value to format
28
+ * @param precision precision
29
+ */
30
+ export declare function toPrecision(value: number, precision?: number): string;
31
+ export declare function getDefaultNetworkInterface(): Promise<string>;
32
+ export declare function checkNetworkInterface(device: string): Promise<void>;
33
+ /** Runs the shell command asynchronously. */
34
+ export declare function runShellCommand(cmd: string, verbose?: boolean): Promise<{
35
+ stdout: string;
36
+ stderr: string;
37
+ }>;
38
+ /** Exit handler callback. */
39
+ export type ExitHandler = (signal?: string) => Promise<void>;
40
+ /**
41
+ * Register an {@link ExitHandler} callback that will be executed at the
42
+ * nodejs process exit.
43
+ * @param exitHandler
44
+ */
45
+ export declare function registerExitHandler(exitHandler: ExitHandler): void;
46
+ /**
47
+ * Un-registers the {@link ExitHandler} callback.
48
+ * @param exitHandler
49
+ */
50
+ export declare function unregisterExitHandler(exitHandler: ExitHandler): void;
51
+ /**
52
+ * Runs the registered exit handlers immediately.
53
+ * @param signal The process exit signal.
54
+ */
55
+ export declare function runExitHandlersNow(signal?: string): Promise<void>;
56
+ export declare function getProcessChildren(pid: number): Promise<number[]>;
57
+ export {};