@torrent-tv/proxy 2.56.0 → 2.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,120 +1,148 @@
1
- /**
2
- * @file The packet witness's gating rules and command construction.
3
- *
4
- * Roadmap item 10: a send queue wedged longer than ~30 s must start a bounded
5
- * tcpdump on its own, because the 2026-08-24 episode proved no counter above
6
- * the wire can name the cause. Everything here is the part that decides WHEN
7
- * and WITH WHAT ARGUMENTS — the spawning itself is thin glue around these.
8
- */
9
-
10
- import assert from "node:assert/strict";
11
- import test from "node:test";
12
-
13
- import {
14
- buildTcpdumpArgs,
15
- createPacketWitness,
16
- isWitnessCapture,
17
- normalizeRemoteAddress,
18
- shouldStartCapture,
19
- WITNESS_CAPTURE_SECONDS,
20
- WITNESS_RING_FILES,
21
- WITNESS_ROTATE_SECONDS
22
- } from "../services/packet-witness.js";
23
-
24
- test("an IPv4 literal survives unchanged", () => {
25
- assert.equal(normalizeRemoteAddress("192.168.178.57"), "192.168.178.57");
26
- });
27
-
28
- test("an IPv6 literal survives unchanged", () => {
29
- assert.equal(
30
- normalizeRemoteAddress("2001:1c00:a603:2100:a129:a1a1:7f07:3f0b"),
31
- "2001:1c00:a603:2100:a129:a1a1:7f07:3f0b"
32
- );
33
- });
34
-
35
- test("a zone suffix is stripped", () => {
36
- assert.equal(normalizeRemoteAddress("fe80::2aca:8001:3ba6:f16f%18"), "fe80::2aca:8001:3ba6:f16f");
37
- });
38
-
39
- test("hostnames, garbage and shell text are rejected", () => {
40
- assert.equal(normalizeRemoteAddress("homeassistant.local"), null);
41
- assert.equal(normalizeRemoteAddress("8.8.8.8; rm -rf /"), null);
42
- assert.equal(normalizeRemoteAddress("999.1.1.1"), null);
43
- assert.equal(normalizeRemoteAddress(""), null);
44
- assert.equal(normalizeRemoteAddress(undefined), null);
45
- assert.equal(normalizeRemoteAddress(42), null);
46
- });
47
-
48
- test("the tcpdump command line is bounded and filtered to one peer", () => {
49
- const args = buildTcpdumpArgs({
50
- host: "2001:db8::1",
51
- port: 9090,
52
- filePrefix: "/data/packet-witness.e2b5ef39.1787600000.pcap"
53
- });
54
- assert.deepEqual(args, [
55
- "-n",
56
- "-S",
57
- "-s",
58
- "128",
59
- "-i",
60
- "any",
61
- "-w",
62
- "/data/packet-witness.e2b5ef39.1787600000.pcap",
63
- "-G",
64
- String(WITNESS_ROTATE_SECONDS),
65
- "-W",
66
- String(WITNESS_RING_FILES),
67
- "udp",
68
- "and",
69
- "host",
70
- "2001:db8::1",
71
- "and",
72
- "port",
73
- "9090"
74
- ]);
75
- });
76
-
77
- test("the capture window is what the rotation ring adds up to", () => {
78
- assert.equal(WITNESS_ROTATE_SECONDS * WITNESS_RING_FILES, WITNESS_CAPTURE_SECONDS);
79
- });
80
-
81
- test("one capture runs at a time", () => {
82
- assert.equal(shouldStartCapture({ running: true, lastStartedAt: Date.now() }), false);
83
- });
84
-
85
- test("the first capture of a process is always allowed", () => {
86
- assert.equal(shouldStartCapture({ running: false, lastStartedAt: 0, now: 1000 }), true);
87
- });
88
-
89
- test("a capture within the cooldown is refused, one after it is allowed", () => {
90
- const state = { running: false, lastStartedAt: 10_000 };
91
- assert.equal(shouldStartCapture({ ...state, now: 10_000 + 599_999 }), false);
92
- assert.equal(shouldStartCapture({ ...state, now: 10_000 + 600_000 }), true);
93
- });
94
-
95
- test("capture files are recognised by name, rotations included", () => {
96
- assert.equal(isWitnessCapture("packet-witness.e2b5ef39.1787600000.pcap"), true);
97
- assert.equal(isWitnessCapture("packet-witness.e2b5ef39.1787600000.pcap20260825T120000"), true);
98
- assert.equal(isWitnessCapture("core.WorkerThread.81.1787600000"), false);
99
- assert.equal(isWitnessCapture("proxy.log"), false);
100
- assert.equal(isWitnessCapture("../packet-witness.x.pcap"), false);
101
- });
102
-
103
- test("a trigger without a usable remote endpoint is refused without touching anything", () => {
104
- const lines = [];
105
- const witness = createPacketWitness({
106
- log: (message) => lines.push(message),
107
- dir: "",
108
- port: 9090
109
- });
110
- const started = witness.maybeCapture({
111
- sessionId: "e2b5ef39-0000",
112
- tag: "e2b5ef39",
113
- label: "proxy",
114
- remote: null,
115
- queuedBytes: 160_000_000,
116
- stuckForMs: 31_000
117
- });
118
- assert.equal(started, false);
119
- assert.deepEqual(lines, []);
120
- });
1
+ /**
2
+ * @file The packet witness's gating rules and command construction.
3
+ *
4
+ * Roadmap item 10: a send queue wedged longer than ~30 s must start a bounded
5
+ * tcpdump on its own, because the 2026-08-24 episode proved no counter above
6
+ * the wire can name the cause. Everything here is the part that decides WHEN
7
+ * and WITH WHAT ARGUMENTS — the spawning itself is thin glue around these.
8
+ */
9
+
10
+ import assert from "node:assert/strict";
11
+ import test from "node:test";
12
+
13
+ import {
14
+ buildTcpdumpArgs,
15
+ createPacketWitness,
16
+ isWitnessCapture,
17
+ isWitnessRingFile,
18
+ normalizeRemoteAddress,
19
+ shouldStartCapture,
20
+ WITNESS_RING_FILE_MB,
21
+ WITNESS_RING_FILES,
22
+ WITNESS_RTO_CEILING_SECONDS,
23
+ WITNESS_TAIL_SECONDS
24
+ } from "../services/packet-witness.js";
25
+
26
+ test("an IPv4 literal survives unchanged", () => {
27
+ assert.equal(normalizeRemoteAddress("192.168.178.57"), "192.168.178.57");
28
+ });
29
+
30
+ test("an IPv6 literal survives unchanged", () => {
31
+ assert.equal(
32
+ normalizeRemoteAddress("2001:1c00:a603:2100:a129:a1a1:7f07:3f0b"),
33
+ "2001:1c00:a603:2100:a129:a1a1:7f07:3f0b"
34
+ );
35
+ });
36
+
37
+ test("a zone suffix is stripped", () => {
38
+ assert.equal(normalizeRemoteAddress("fe80::2aca:8001:3ba6:f16f%18"), "fe80::2aca:8001:3ba6:f16f");
39
+ });
40
+
41
+ test("hostnames, garbage and shell text are rejected", () => {
42
+ assert.equal(normalizeRemoteAddress("homeassistant.local"), null);
43
+ assert.equal(normalizeRemoteAddress("8.8.8.8; rm -rf /"), null);
44
+ assert.equal(normalizeRemoteAddress("999.1.1.1"), null);
45
+ assert.equal(normalizeRemoteAddress(""), null);
46
+ assert.equal(normalizeRemoteAddress(undefined), null);
47
+ assert.equal(normalizeRemoteAddress(42), null);
48
+ });
49
+
50
+ test("the tcpdump command line is bounded and filtered to one peer", () => {
51
+ const args = buildTcpdumpArgs({
52
+ host: "2001:db8::1",
53
+ port: 9090,
54
+ filePrefix: "/data/packet-witness.e2b5ef39.1787600000.pcap"
55
+ });
56
+ assert.deepEqual(args, [
57
+ "-n",
58
+ "-S",
59
+ "-s",
60
+ "128",
61
+ "-i",
62
+ "any",
63
+ "-w",
64
+ "/data/packet-witness.e2b5ef39.1787600000.pcap",
65
+ "-C",
66
+ String(WITNESS_RING_FILE_MB),
67
+ "-W",
68
+ String(WITNESS_RING_FILES),
69
+ "udp",
70
+ "and",
71
+ "port",
72
+ "9090",
73
+ "and",
74
+ "host",
75
+ "2001:db8::1"
76
+ ]);
77
+ });
78
+
79
+ test("rotation is by size, so the ring files cannot collapse onto one name", () => {
80
+ // `-G` with a name carrying no strftime field overwrote a single file, which
81
+ // is why both field captures held 28 s instead of the intended 120.
82
+ const args = buildTcpdumpArgs({ port: 9090, filePrefix: "/data/ring.pcap" });
83
+ assert.equal(args.includes("-G"), false);
84
+ assert.equal(args[args.indexOf("-C") + 1], String(WITNESS_RING_FILE_MB));
85
+ });
86
+
87
+ test("with no peer named the ring records every peer on the port", () => {
88
+ const args = buildTcpdumpArgs({ port: 9090, filePrefix: "/data/ring.pcap" });
89
+ assert.equal(args.includes("host"), false);
90
+ assert.deepEqual(args.slice(-4), ["udp", "and", "port", "9090"]);
91
+ });
92
+
93
+ test("the tail outlasts three retransmission timeouts", () => {
94
+ assert.equal(WITNESS_TAIL_SECONDS, WITNESS_RTO_CEILING_SECONDS * 3);
95
+ // A zero-window probe is due once per timeout, so a window shorter than the
96
+ // ceiling could not tell silence from a probe that had not come round yet.
97
+ assert.ok(WITNESS_TAIL_SECONDS > WITNESS_RTO_CEILING_SECONDS);
98
+ });
99
+
100
+ test("ring files are told apart from the copies kept as evidence", () => {
101
+ assert.equal(isWitnessRingFile("packet-witness-ring.pcap"), true);
102
+ assert.equal(isWitnessRingFile("packet-witness-ring.pcap3"), true);
103
+ assert.equal(isWitnessRingFile("packet-witness.e2b5ef39.1787600000.before1.pcap"), false);
104
+ assert.equal(isWitnessCapture("packet-witness-ring.pcap3"), false);
105
+ assert.equal(isWitnessCapture("packet-witness.e2b5ef39.1787600000.before1.pcap"), true);
106
+ assert.equal(isWitnessCapture("packet-witness.e2b5ef39.1787600000.tail.pcap1"), true);
107
+ });
108
+
109
+ test("one capture runs at a time", () => {
110
+ assert.equal(shouldStartCapture({ running: true, lastStartedAt: Date.now() }), false);
111
+ });
112
+
113
+ test("the first capture of a process is always allowed", () => {
114
+ assert.equal(shouldStartCapture({ running: false, lastStartedAt: 0, now: 1000 }), true);
115
+ });
116
+
117
+ test("a capture within the cooldown is refused, one after it is allowed", () => {
118
+ const state = { running: false, lastStartedAt: 10_000 };
119
+ assert.equal(shouldStartCapture({ ...state, now: 10_000 + 599_999 }), false);
120
+ assert.equal(shouldStartCapture({ ...state, now: 10_000 + 600_000 }), true);
121
+ });
122
+
123
+ test("capture files are recognised by name, rotations included", () => {
124
+ assert.equal(isWitnessCapture("packet-witness.e2b5ef39.1787600000.pcap"), true);
125
+ assert.equal(isWitnessCapture("packet-witness.e2b5ef39.1787600000.pcap20260825T120000"), true);
126
+ assert.equal(isWitnessCapture("core.WorkerThread.81.1787600000"), false);
127
+ assert.equal(isWitnessCapture("proxy.log"), false);
128
+ assert.equal(isWitnessCapture("../packet-witness.x.pcap"), false);
129
+ });
130
+
131
+ test("a trigger without a usable remote endpoint is refused without touching anything", () => {
132
+ const lines = [];
133
+ const witness = createPacketWitness({
134
+ log: (message) => lines.push(message),
135
+ dir: "",
136
+ port: 9090
137
+ });
138
+ const started = witness.maybeCapture({
139
+ sessionId: "e2b5ef39-0000",
140
+ tag: "e2b5ef39",
141
+ label: "proxy",
142
+ remote: null,
143
+ queuedBytes: 160_000_000,
144
+ stuckForMs: 31_000
145
+ });
146
+ assert.equal(started, false);
147
+ assert.deepEqual(lines, []);
148
+ });
@@ -0,0 +1,131 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { wedgeIsCertain } from "../services/data-channel-handler.js";
5
+ import { PROBE_INTERVAL_MS } from "../services/delivery-probe.js";
6
+
7
+ const MEGABYTE = 1024 * 1024;
8
+
9
+ test("a queue draining at the rate this link was measured at is not a wedge", () => {
10
+ // 8 MB at 16 MB/s is half a second of draining. Half a second in is normal.
11
+ const verdict = wedgeIsCertain({
12
+ queuedBytes: 8 * MEGABYTE,
13
+ bytesPerSecond: 16 * MEGABYTE,
14
+ flatForMs: 200
15
+ });
16
+ assert.equal(verdict.certain, false);
17
+ });
18
+
19
+ test("the counter standing still past the queue's own drain time is a wedge", () => {
20
+ const verdict = wedgeIsCertain({
21
+ queuedBytes: 8 * MEGABYTE,
22
+ bytesPerSecond: 16 * MEGABYTE,
23
+ flatForMs: 4000
24
+ });
25
+ assert.equal(verdict.certain, true);
26
+ assert.equal(verdict.needMs, 500);
27
+ });
28
+
29
+ test("a big queue on a slow link is given the time it genuinely needs", () => {
30
+ // 67 MB at 1 MB/s is 67 seconds of honest draining — the field's own numbers
31
+ // for the wedged channel, at a rate a thin link could really be running at.
32
+ const patient = wedgeIsCertain({
33
+ queuedBytes: 67 * MEGABYTE,
34
+ bytesPerSecond: MEGABYTE,
35
+ flatForMs: 30_000
36
+ });
37
+ assert.equal(patient.certain, false);
38
+ const later = wedgeIsCertain({
39
+ queuedBytes: 67 * MEGABYTE,
40
+ bytesPerSecond: MEGABYTE,
41
+ flatForMs: 70_000
42
+ });
43
+ assert.equal(later.certain, true);
44
+ });
45
+
46
+ test("the same queue on the link the field actually had is called quickly", () => {
47
+ // 67 MB at 18 MB/s is under four seconds. The field episode stood still for
48
+ // 3217 s; the previous rule waited a flat 30 s before recording anything.
49
+ const verdict = wedgeIsCertain({
50
+ queuedBytes: 67 * MEGABYTE,
51
+ bytesPerSecond: 18 * MEGABYTE,
52
+ flatForMs: 5000
53
+ });
54
+ assert.equal(verdict.certain, true);
55
+ assert.ok(verdict.needMs < 30_000);
56
+ });
57
+
58
+ test("a tiny queue still waits for one round of probes", () => {
59
+ const verdict = wedgeIsCertain({
60
+ queuedBytes: 512,
61
+ bytesPerSecond: 16 * MEGABYTE,
62
+ flatForMs: PROBE_INTERVAL_MS - 1
63
+ });
64
+ assert.equal(verdict.certain, false);
65
+ assert.equal(verdict.needMs, PROBE_INTERVAL_MS);
66
+ });
67
+
68
+ test("nothing queued is not a wedge however long the counter has been still", () => {
69
+ const verdict = wedgeIsCertain({
70
+ queuedBytes: 0,
71
+ bytesPerSecond: 16 * MEGABYTE,
72
+ flatForMs: 600_000
73
+ });
74
+ assert.equal(verdict.certain, false);
75
+ assert.equal(verdict.needMs, null);
76
+ });
77
+
78
+ test("with no rate measured the answer is that nothing can be said", () => {
79
+ const verdict = wedgeIsCertain({
80
+ queuedBytes: 8 * MEGABYTE,
81
+ bytesPerSecond: 0,
82
+ flatForMs: 600_000
83
+ });
84
+ assert.equal(verdict.certain, false);
85
+ assert.equal(verdict.needMs, null);
86
+ });
87
+
88
+ test("a pause no longer than this link's own longest healthy pause is not a wedge", () => {
89
+ // A retransmission timeout stops the accepted-byte counter dead: a full send
90
+ // buffer accepts nothing. If this connection has already paused 3 s while
91
+ // healthy, a 3 s pause says nothing.
92
+ const verdict = wedgeIsCertain({
93
+ queuedBytes: 8 * MEGABYTE,
94
+ bytesPerSecond: 18 * MEGABYTE,
95
+ flatForMs: 2500,
96
+ longestHealthyFlatMs: 3000
97
+ });
98
+ assert.equal(verdict.certain, false);
99
+ assert.equal(verdict.needMs, 3000);
100
+ });
101
+
102
+ test("a pause longer than any this link has shown is a wedge", () => {
103
+ const verdict = wedgeIsCertain({
104
+ queuedBytes: 8 * MEGABYTE,
105
+ bytesPerSecond: 18 * MEGABYTE,
106
+ flatForMs: 3500,
107
+ longestHealthyFlatMs: 3000
108
+ });
109
+ assert.equal(verdict.certain, true);
110
+ });
111
+
112
+ test("the quiet-probe rate cannot be what the queue is divided by", () => {
113
+ // With the browser's buffer full, the only traffic is the probe: three
114
+ // channels, a few dozen bytes, twice a second. Dividing 8 MB by that gives
115
+ // six hours, and the wedge would never be called. The BEST rate seen is what
116
+ // the watcher keeps, so this case must not arise — pinned here as the
117
+ // arithmetic that made it matter.
118
+ const wrong = wedgeIsCertain({
119
+ queuedBytes: 8 * MEGABYTE,
120
+ bytesPerSecond: 360,
121
+ flatForMs: 60_000
122
+ });
123
+ assert.equal(wrong.certain, false);
124
+ assert.ok(wrong.needMs > 6 * 60 * 60 * 1000 - 1);
125
+ const right = wedgeIsCertain({
126
+ queuedBytes: 8 * MEGABYTE,
127
+ bytesPerSecond: 18 * MEGABYTE,
128
+ flatForMs: 60_000
129
+ });
130
+ assert.equal(right.certain, true);
131
+ });