@mark1russell7/harness 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -46,6 +46,14 @@ ioredis sets the keepalive of its socket itself, after it connects. Thus a cell
46
46
  - **A reset** is a `REJECT --reject-with tcp-reset` rule in the namespace of the target, for the segments of the client. The next send of the client gets a reset one round trip later. It is the reference fault of the calibration.
47
47
  - **A frozen process** is the cgroup freezer (`docker pause`), while the kernel stays up.
48
48
  - **A stall** is a black hole that stops by itself, `Cell.stall` after the send of the request. The run reads the stamp of the client on the send. Then `inject clear PEER --at NS` holds the end in the VM until that instant (`lab.heal`). Thus the latency of a docker exec does not change the length of the stall.
49
+ - **An ICMP unreachable** (`unreachable`, `Cell.icmp`) answers each segment from the observer with host, net or port unreachable, or admin prohibited. Linux keeps it as a soft error, continues to retransmit, and gives the errno of the ICMP at the timeout. Host and net unreachable also revert the backoff (RFC 6069).
50
+ - **A simplex partition** (`simplex`, `Cell.drops`) drops the segments to the server, or the segments to the client, but not both.
51
+ - **A FIN** (`fin`) stops the process of the peer with SIGKILL while its kernel stays up. For the client, the target starts its server as the child of a shell (`Target.command`), and `Target.kill` stops it. For the server, the harness stops the container of the client.
52
+ - **An MTU black hole** (`mtu-blackhole`, `Cell.mtu`) drops each segment longer than the length, in the namespace that receives it.
53
+ - **netem** (`netem`, `Cell.netem`) is tc netem on the egress of the target: loss, duplication, reordering or corruption. The outcome is a distribution.
54
+ - **A latency** (`Cell.latency`) is tc netem delay on the egress of the target before the client connects. Thus the round trip and the RTO include it.
55
+
56
+ A length match and netem act on GSO super-packets of up to 64 KB, not on segments, while TSO, GSO and GRO are on. Thus the cells of `mtu-blackhole` and `netem` turn them off on the two ends (`lab.offload`).
49
57
 
50
58
  Each run starts from kernels that keep nothing from the run before it (`lab.forget()`). When a connection closes, each kernel keeps TCP metrics for the address of its peer. These metrics set the first round trip of the next connection, and thus its RTO. The harness removes the `tcp_metrics` of the two namespaces before each run.
51
59
 
@@ -59,6 +67,8 @@ Each run starts from kernels that keep nothing from the run before it (`lab.forg
59
67
  - **Void runs.** A run is void when the timer that drives the measurement was two jiffies or more behind its wheel slot in the timed interval. The wheel does a slot in the tick when it is due. Thus such a firing measured the host, which stopped a vCPU, not the kernel. A keepalive timer that found its socket held also makes the run void. A run whose fault came too late is void too.
60
68
  - **Findings.** A finding is a wrong errno, a detector that did not fire, or a detection where the prediction is silence.
61
69
  - **Recovery.** After a stall (`prediction.recovers`), it times the send to the first segment back after the stall stopped. That segment is the answer of the server to the retransmission that got through. A run whose stall stopped outside the window of its prediction is void. A run where nothing came back is a finding.
70
+ - **An end of file.** A FIN counts from the change of state of the socket of the peer when it closes (`peer-closed`). It ends at the change of the socket of the observer to CLOSE_WAIT. The kernel stamps the two changes.
71
+ - **A stream.** In a stream (`probe.mode` is `stream`), the client sends `count` requests. The run keeps the time from each send to the first data after it (`samples`).
62
72
 
63
73
  ## The verdict
64
74
 
@@ -68,6 +78,7 @@ Each run starts from kernels that keep nothing from the run before it (`lab.forg
68
78
  - **Median equivalence.** The order-statistic interval of the median must be in the band, made 5 % wider.
69
79
  - **Extension.** A median across the edge of the band extends the cell from 11 runs to 22, then 44. Then the verdict is inconclusive.
70
80
  - **Recovery.** The recovery after a stall gets the same test as a detection, by its band. A detection is then a finding.
81
+ - **Measurements.** A cell whose outcome is a distribution (`detects: null`) has no band. Its decision is `measured`, and the verdict gives the median and the slowest reply time.
71
82
 
72
83
  The distribution test needs a simulator, which does not exist at this time.
73
84
 
package/build/index.d.ts CHANGED
@@ -11,16 +11,25 @@ import { KernelRecord } from "@mark1russell7/prober";
11
11
  * It is the reference for the noise of the harness itself.
12
12
  * - `stall` is a black hole that stops by itself, `Cell.stall` after the client sends its
13
13
  * request. The VM times it. It shows what recovers, and when.
14
+ * - `fin` stops the process of the peer with SIGKILL, while its kernel stays up. The kernel closes its sockets
15
+ * with a FIN, or with a reset when unread data is in the queue.
16
+ * - `unreachable` answers each segment from the observer with an ICMP unreachable (`Cell.icmp`).
17
+ * Linux keeps it as a soft error on an established socket, and it continues to retransmit.
18
+ * - `simplex` drops the segments in one direction only (`Cell.drops`).
19
+ * - `mtu-blackhole` drops each segment to the observer that is longer than `Cell.mtu` bytes.
20
+ * - `netem` is tc netem on the egress of the target (`Cell.netem`): loss, duplication,
21
+ * reordering or corruption. Its outcome is a distribution.
14
22
  */
15
- type Fault = "blackhole" | "freeze-process" | "rst" | "stall";
23
+ type Fault = "blackhole" | "freeze-process" | "rst" | "stall" | "fin" | "unreachable" | "simplex" | "mtu-blackhole" | "netem";
16
24
  /**
17
25
  * A reference is the event that the time of a detector counts from: the event that arms the
18
26
  * timer in its machine document. Keepalive counts from the last received segment. The give-up of
19
27
  * the retransmissions counts from the first retransmission of the RTO timer, not from the tail
20
28
  * loss probe one RTO before it. The request itself meets a reset, thus a reset counts from the
21
- * send of the client.
29
+ * send of the client. A FIN counts from the change of state of the socket of the peer when its
30
+ * process dies (`peer-closed`).
22
31
  */
23
- type Reference = "last-received" | "first-retransmission" | "request-sent";
32
+ type Reference = "last-received" | "first-retransmission" | "request-sent" | "peer-closed";
24
33
  /**
25
34
  * This prediction tells when a connection moves again after a stall. That occurs at the first
26
35
  * firing of its retransmission timer after the stall stops, and its reply comes back one round
@@ -48,11 +57,15 @@ interface Recovers {
48
57
  * after the reference event, or nothing detects the fault in a window. Nothing detects a stall
49
58
  * that is shorter than the give-up, and the connection recovers from it. The prediction then also
50
59
  * tells when.
60
+ *
61
+ * A cell whose outcome is a distribution, for example under netem loss, has no band. Its
62
+ * prediction is `detects: null` with the reason, and its runs are measurements only.
51
63
  */
52
64
  type Prediction = {
53
65
  readonly detects: true;
54
66
  /** This name is the timer of the machine that fires first, or the event that detects the fault, with its errno. */
55
67
  readonly timer: string;
68
+ /** This name is the errno of the detection, or EOF for an end of file without an error. */
56
69
  readonly errno: string;
57
70
  readonly from: Reference;
58
71
  /** This time is the closed form, less the most that the timer can fire early. */
@@ -63,6 +76,10 @@ type Prediction = {
63
76
  readonly detects: false;
64
77
  readonly window: Quantity;
65
78
  readonly recovers?: Recovers;
79
+ } | {
80
+ readonly detects: null;
81
+ readonly window: Quantity;
82
+ readonly why: string;
66
83
  };
67
84
  /** A target is the server that a cell connects to: a stock image. */
68
85
  interface Target {
@@ -80,6 +97,13 @@ interface Target {
80
97
  readonly set: readonly string[];
81
98
  readonly get: readonly string[];
82
99
  };
100
+ /**
101
+ * This command replaces the command of the image, for example a shell that holds the server
102
+ * process as its child. Then a `fin` fault can stop the server, and the container stays up.
103
+ */
104
+ readonly command?: readonly string[];
105
+ /** This command stops the server process in the target with SIGKILL, as argv, for a `fin` fault. */
106
+ readonly kill?: readonly string[];
83
107
  }
84
108
  /** A client is what the runs of a cell use: the TCP client of the harness, or a driver on its socket. */
85
109
  type Client = "tcp" | "ioredis";
@@ -97,8 +121,15 @@ interface Cell {
97
121
  /** These are the sysctls of the network namespace of the client, as the kernel spells them. The harness sets them before the client connects. */
98
122
  readonly sysctls: Readonly<Record<string, string>>;
99
123
  readonly probe: {
100
- /** This mode tells if the client is idle after its first reply, or sends a request into the fault. */
101
- readonly mode: "idle" | "inflight";
124
+ /**
125
+ * This mode tells if the client is idle after its first reply, or sends a request into the
126
+ * fault. In a stream, it sends `count` requests into it, each `interval` after the reply before.
127
+ */
128
+ readonly mode: "idle" | "inflight" | "stream";
129
+ /** This count is the number of requests of a stream. */
130
+ readonly count?: number;
131
+ /** This time is the pause after each reply of a stream, before the next request. */
132
+ readonly interval?: Quantity;
102
133
  /** This flag sets SO_KEEPALIVE without overrides for the socket: the keepalive sysctls of the namespace apply. */
103
134
  readonly keepalive: boolean;
104
135
  /** This time sets TCP_USER_TIMEOUT. Without it, the default of the kernel (off) applies. */
@@ -122,6 +153,21 @@ interface Cell {
122
153
  readonly fault: Fault;
123
154
  /** This time is the length of a stall, from the send of the request. */
124
155
  readonly stall?: Quantity;
156
+ /** This code is the ICMP unreachable of an `unreachable` fault. */
157
+ readonly icmp?: "host" | "net" | "port" | "admin";
158
+ /** This direction is the one that a `simplex` fault drops: the segments to the server, or to the client. */
159
+ readonly drops?: "to-server" | "to-client";
160
+ /** This length is the longest segment, in bytes, that an `mtu-blackhole` fault lets through to the observer. */
161
+ readonly mtu?: number;
162
+ /** This specification is the tc netem of a `netem` fault, on the egress of the target: for example "loss 5%". */
163
+ readonly netem?: string;
164
+ /**
165
+ * This delay goes on the egress of the target before the client connects, with tc netem. Thus
166
+ * each round trip is longer by the delay, and so is the RTO.
167
+ */
168
+ readonly latency?: Quantity;
169
+ /** These commands run in the target before a client connects, each as argv: for example, a large value to read. */
170
+ readonly targetSetup?: readonly (readonly string[])[];
125
171
  readonly prediction: Prediction;
126
172
  /** This side is the side whose detection the cell measures. Without it, the side is the client. */
127
173
  readonly observer?: Observer;
@@ -207,6 +253,11 @@ interface Measured {
207
253
  * the RTO timer. After a stall, they are each retransmission from the send, the loss probe first.
208
254
  */
209
255
  readonly steps: readonly Quantity[];
256
+ /**
257
+ * These are the reply times of a measurement-only cell. For each request after the first, a
258
+ * sample is the time from its send to the first data after it.
259
+ */
260
+ readonly samples?: readonly Quantity[];
210
261
  /** This count is the number of keepalive probes that the kernel sent. */
211
262
  readonly probes: number;
212
263
  /** This count is the number of segments that the kernel sent again. */
@@ -241,8 +292,11 @@ export declare const NOISE: Quantity;
241
292
  export declare const RELATIVE = .05;
242
293
  /** These are the run counts of a cell: 11, then 22 and 44 while its median crosses the edge of the band. */
243
294
  export declare const STEPS: readonly [11, 22, 44];
244
- /** A decision is the result of a verdict. */
245
- type Decision = "holds" | "fails" | "extend" | "inconclusive";
295
+ /**
296
+ * A decision is the result of a verdict. A cell whose outcome is a distribution has no band, and
297
+ * its decision is `measured`: a record, not a test.
298
+ */
299
+ type Decision = "holds" | "fails" | "extend" | "inconclusive" | "measured";
246
300
  /** An interval is a range of times. */
247
301
  interface Interval {
248
302
  readonly low: Quantity;
@@ -290,8 +344,20 @@ export declare const verdictOf: (prediction: Prediction, runs: readonly Measured
290
344
  export declare const harnessImage: () => Promise<string>;
291
345
  /** This function gives the image of the Node client. */
292
346
  export declare const nodeClientImage: () => Promise<string>;
293
- /** A network fault is a fault that the injector applies, by the name of its manifestation. */
294
- type NetworkFault = "blackhole" | "rst";
347
+ /**
348
+ * A network fault is a fault that the injector applies as a filter rule. It is a black hole (both
349
+ * directions, or one), or a reset on the next segment. It can also be an ICMP unreachable for each
350
+ * segment, or a drop of each segment that is longer than a length.
351
+ */
352
+ type NetworkFault = "blackhole" | "rst" | "reject" | "mtu";
353
+ /** These are the options of a network fault: the direction of a black hole, the code of a reject, the length of an MTU drop. */
354
+ interface FaultOptions {
355
+ /** This direction is relative to the namespace of the rule: `in` drops what comes from the peer, `out` what goes to it. */
356
+ readonly direction?: "in" | "out";
357
+ readonly code?: "host" | "net" | "port" | "admin";
358
+ /** This length is the longest segment in bytes that an MTU drop lets through. */
359
+ readonly above?: number;
360
+ }
295
361
  /**
296
362
  * A fault side is the namespace that gets the rules of a fault: the namespace of the peer of the
297
363
  * observer. For the client, it is the namespace of the target (the injector). For the server, it
@@ -344,6 +410,8 @@ interface Probe {
344
410
  /** This method freezes the client with the cgroup freezer: its process stops, and its kernel continues to answer. */
345
411
  freeze(): Promise<void>;
346
412
  thaw(): Promise<void>;
413
+ /** This method stops the process of the client with SIGKILL. Its kernel then closes its socket with a FIN, or a reset. */
414
+ kill(): Promise<void>;
347
415
  readonly events: readonly ProbeEvent[];
348
416
  stop(): Promise<void>;
349
417
  }
@@ -368,12 +436,35 @@ interface Lab {
368
436
  * target. The fault is a black hole, or a reset that answers the next segment of the client. The stamps of
369
437
  * the injector bracket it.
370
438
  */
371
- fault(kind: NetworkFault, at?: FaultSide): Promise<{
439
+ fault(kind: NetworkFault, at?: FaultSide, options?: FaultOptions): Promise<{
372
440
  t0: number;
373
441
  t1: number;
374
442
  }>;
375
443
  /** This method removes a fault. */
376
- lift(kind: NetworkFault, at?: FaultSide): Promise<void>;
444
+ lift(kind: NetworkFault, at?: FaultSide, options?: FaultOptions): Promise<void>;
445
+ /**
446
+ * This method puts tc netem on the egress of a namespace, for example "delay 20ms" or "loss 5%",
447
+ * or removes it with null. The stamps of the injector bracket it.
448
+ */
449
+ netem(spec: string | null, at?: FaultSide): Promise<{
450
+ t0: number;
451
+ t1: number;
452
+ }>;
453
+ /**
454
+ * This method sets the TSO, GSO and GRO of a namespace. With them off, netem and a length match
455
+ * act on segments, not on GSO super-packets of up to 64 KB.
456
+ */
457
+ offload(on: boolean, at?: FaultSide): Promise<void>;
458
+ /** This method stops the server process in the target with SIGKILL, with the command of the target. */
459
+ kill(): Promise<void>;
460
+ /**
461
+ * This method resolves the neighbors of the path in the two directions, with a ping from each
462
+ * namespace. Without it, the first connection of an environment waits for ARP. Under a netem
463
+ * delay, that wait is one more delayed round, and its round trip and RTO are longer.
464
+ */
465
+ warm(): Promise<void>;
466
+ /** This method runs commands in the target before a client connects, each as argv. */
467
+ setup(commands: ReadonlyArray<readonly string[]>): Promise<void>;
377
468
  /**
378
469
  * This method stops a black hole at an instant on the monotonic clock of the VM. The injector
379
470
  * waits for that instant in the VM, thus the length of a stall does not include the latency of
package/build/index.js CHANGED
@@ -18,10 +18,13 @@ const ERRNO = {
18
18
  };
19
19
  /** This function gives the interval between two instants on the one clock, in seconds to the microsecond. */
20
20
  const between = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
21
+ /** These functions give the states of a state record: before the change, and after it. TCP_ESTABLISHED is 1, TCP_CLOSE_WAIT 8. */
22
+ const oldState = (record) => record.old;
23
+ const newState = (record) => record.new;
21
24
  const count = (event, field) => event === void 0 ? 0 : Number(event[field] ?? 0);
22
25
  /** This function gives the view of the client on the same interval: TCP_INFO, with a sample each 2 ms. */
23
26
  const socketVantage = (cell, events, ready, request) => {
24
- if (!cell.prediction.detects) return null;
27
+ if (!cell.prediction.detects || cell.prediction.from === "peer-closed") return null;
25
28
  const ended = events.find((event) => event.ev === "error" || event.ev === "eof");
26
29
  const reference = cell.prediction.from === "last-received" ? ready : cell.prediction.from === "request-sent" ? request : events.find((event) => event.ev === "kernel" && event.t >= request.t && count(event, "retransmits") > 0);
27
30
  return ended === void 0 || reference === void 0 ? null : between(reference.t, ended.t);
@@ -38,11 +41,14 @@ const measure = (cell, run) => {
38
41
  const { events, kernel } = run;
39
42
  const ready = events.find((event) => event.ev === "ready");
40
43
  const local = events.find((event) => event.ev === "connected")?.["local"];
41
- const request = cell.probe.mode === "inflight" ? events.filter((event) => event.ev === "sent")[1] : ready;
44
+ const inflight = cell.probe.mode !== "idle";
45
+ const request = inflight ? events.filter((event) => event.ev === "sent")[1] : ready;
42
46
  const server = cell.observer === "server";
43
47
  const netns = server ? run.targetNetns : run.netns;
44
48
  const cookie = kernel.find((record) => record.netns === netns && (server ? record.dst : record.src) === local)?.cookie;
45
49
  const socket = kernel.filter((record) => record.netns === netns && record.cookie === cookie);
50
+ const peerNetns = server ? run.netns : run.targetNetns;
51
+ const peerCookie = kernel.find((record) => record.netns === peerNetns && (server ? record.src : record.dst) === local)?.cookie;
46
52
  const keepalives = socket.filter((record) => record.ev === "probe" && record.what === "keepalive");
47
53
  const retransmitted = socket.filter((record) => record.ev === "retrans");
48
54
  /**
@@ -103,24 +109,42 @@ const measure = (cell, run) => {
103
109
  };
104
110
  let voided = null;
105
111
  if (run.fault !== null) {
112
+ const replyFirst = server && inflight && cell.fault !== "mtu-blackhole";
106
113
  const replied = events.some((event) => event.ev === "data" && event.t > request.t && event.t < run.fault.t1);
107
- if (cell.probe.mode === "inflight" && !server && run.fault.t1 > request.t) voided = "the request went out before the fault held";
108
- else if (cell.probe.mode === "inflight" && server && run.fault.t1 < request.t) voided = "the request went out into the fault, so the server never had it";
109
- else if (cell.probe.mode === "inflight" && server && replied) voided = "the server's reply arrived before the fault held";
114
+ if (inflight && !replyFirst && run.fault.t1 > request.t) voided = "the request went out before the fault held";
115
+ else if (replyFirst && run.fault.t1 < request.t) voided = "the request went out into the fault, so the server never had it";
116
+ else if (replyFirst && replied) voided = "the server's reply arrived before the fault held";
110
117
  else if (keepalives[0] !== void 0 && keepalives[0].mono < run.fault.t1) voided = "a keepalive probe went out before the fault held";
111
118
  }
112
119
  const reported = socket.find((record) => record.ev === "error" && (record.err ?? 0) !== 0);
113
120
  const seen = server ? void 0 : events.find((event) => event.ev === "error" || event.ev === "eof");
121
+ const closeWait = socket.find((record) => record.ev === "state" && newState(record) === 8);
114
122
  const outcome = reported !== void 0 ? {
115
123
  kind: "error",
116
124
  errno: ERRNO[reported.err] ?? `E${reported.err}`
117
- } : seen?.ev === "eof" ? { kind: "eof" } : seen !== void 0 ? {
125
+ } : closeWait !== void 0 || seen?.ev === "eof" ? { kind: "eof" } : seen !== void 0 ? {
118
126
  kind: "error",
119
127
  errno: String(seen["errno"])
120
128
  } : { kind: "silent" };
121
- const ended = reported?.mono ?? seen?.t;
129
+ const ended = reported?.mono ?? closeWait?.mono ?? seen?.t;
122
130
  const noticed = reported !== void 0 && seen !== void 0 ? between(reported.mono, seen.t) : null;
123
131
  const { prediction } = cell;
132
+ if (prediction.detects === null) {
133
+ const sends = events.filter((event) => event.ev === "sent").slice(1);
134
+ const samples = sends.flatMap((send, n) => {
135
+ const next = sends[n + 1]?.t ?? Infinity;
136
+ const reply = events.find((event) => event.ev === "data" && event.t > send.t && event.t < next);
137
+ return reply === void 0 ? [] : [between(send.t, reply.t)];
138
+ });
139
+ return {
140
+ ...base,
141
+ ...nothing,
142
+ outcome: ended !== void 0 ? outcome : { kind: "answered" },
143
+ steps: retransmitted.filter((record) => record.mono > request.t).map((record) => between(request.t, record.mono)),
144
+ samples,
145
+ void: voided
146
+ };
147
+ }
124
148
  if (!prediction.detects && prediction.recovers !== void 0) {
125
149
  const { recovers } = prediction;
126
150
  const heal = run.heal ?? null;
@@ -183,7 +207,7 @@ const measure = (cell, run) => {
183
207
  finding: `nothing detected it: ${prediction.timer} never fired`
184
208
  };
185
209
  const before = run.fault?.t1 ?? ready.t;
186
- const kernelReference = prediction.from === "last-received" ? socket.filter((record) => record.ev === "rx" && record.mono <= before).at(-1) : prediction.from === "first-retransmission" ? retransmitted.find((record) => record.why === "rto") : void 0;
210
+ const kernelReference = prediction.from === "last-received" ? socket.filter((record) => record.ev === "rx" && record.mono <= before).at(-1) : prediction.from === "first-retransmission" ? retransmitted.find((record) => record.why === "rto") : prediction.from === "peer-closed" ? kernel.find((record) => record.netns === peerNetns && record.cookie === peerCookie && record.ev === "state" && oldState(record) === 1) : void 0;
187
211
  const reference = prediction.from === "request-sent" ? request.t : kernelReference?.mono;
188
212
  if (reference === void 0) return {
189
213
  ...base,
@@ -194,8 +218,8 @@ const measure = (cell, run) => {
194
218
  finding: `it ended without its reference event (${prediction.from})`
195
219
  };
196
220
  const steps = (prediction.from === "last-received" ? keepalives : prediction.from === "first-retransmission" ? retransmitted.filter((record) => record.why === "rto" && record.mono > reference) : []).map((record) => between(reference, record.mono));
197
- const finding = outcome.kind === "error" && outcome.errno === prediction.errno ? null : `it ended with ${outcome.kind === "error" ? outcome.errno : "end of file"}, not ${prediction.errno}`;
198
- if (prediction.from !== "request-sent") voided ??= delayed(reference, ended, prediction.from === "last-received" ? "keepalive" : "retransmit");
221
+ const finding = (prediction.errno === "EOF" ? outcome.kind === "eof" : outcome.kind === "error" && outcome.errno === prediction.errno) ? null : `it ended with ${outcome.kind === "error" ? outcome.errno : "end of file"}, not ${prediction.errno}`;
222
+ if (prediction.from !== "request-sent" && prediction.from !== "peer-closed") voided ??= delayed(reference, ended, prediction.from === "last-received" ? "keepalive" : "retransmit");
199
223
  return {
200
224
  ...base,
201
225
  outcome,
@@ -284,6 +308,20 @@ const verdictOf = (prediction, runs) => {
284
308
  outside,
285
309
  why: "no run counted"
286
310
  };
311
+ if (prediction.detects === null) {
312
+ const samples = counted.flatMap((run) => (run.samples ?? []).map((sample) => valueIn(sample, "s"))).sort((a, b) => a - b);
313
+ const retransmissions = counted.reduce((sum, run) => sum + run.retransmissions, 0);
314
+ const ended = counted.filter((run) => run.outcome.kind === "error" || run.outcome.kind === "eof");
315
+ const why = samples.length === 0 ? `${counted.length} runs measured, with no reply: ${prediction.why}` : `${counted.length} runs, ${samples.length} replies: median ${s(middle(samples)).value} s, slowest ${s(samples.at(-1)).value} s, ${retransmissions} retransmissions, ${ended.length} connections ended. ${prediction.why}`;
316
+ return {
317
+ ...empty,
318
+ decision: "measured",
319
+ band: null,
320
+ outside: [],
321
+ median: samples.length === 0 ? null : s(middle(samples)),
322
+ why
323
+ };
324
+ }
287
325
  if (!prediction.detects && prediction.recovers === void 0) {
288
326
  const decision = outside.length === 0 ? "holds" : "fails";
289
327
  const why = decision === "holds" ? `nothing detected it in ${counted.length} runs of ${valueIn(prediction.window, "s")} s` : `${outside.length} of ${counted.length} runs detected it`;
@@ -457,6 +495,19 @@ const build = (image) => {
457
495
  const harnessImage = () => build(HARNESS);
458
496
  /** This function gives the image of the Node client. */
459
497
  const nodeClientImage = () => build(NODE);
498
+ /** This function gives the arguments of the injector for the options of a fault. */
499
+ const optionArgs = (options) => [
500
+ ...options.direction === void 0 ? [] : ["--direction", options.direction],
501
+ ...options.code === void 0 ? [] : ["--code", options.code],
502
+ ...options.above === void 0 ? [] : ["--above", String(options.above)]
503
+ ];
504
+ /** These are the actions of the injector that lift each network fault. */
505
+ const LIFT = {
506
+ blackhole: "clear",
507
+ rst: "clear-rst",
508
+ reject: "clear-reject",
509
+ mtu: "clear-mtu"
510
+ };
460
511
  const lines = () => {
461
512
  const seen = [];
462
513
  const other = [];
@@ -539,7 +590,8 @@ const openLab = async (target, kind = "tcp") => {
539
590
  };
540
591
  try {
541
592
  network = await new Network().start();
542
- const server = await new GenericContainer(target.image).withNetwork(network).withWaitStrategy(Wait.forLogMessage(target.ready)).start();
593
+ const stock = new GenericContainer(target.image).withNetwork(network).withWaitStrategy(Wait.forLogMessage(target.ready));
594
+ const server = await (target.command === void 0 ? stock : stock.withCommand([...target.command])).start();
543
595
  opened.push(server);
544
596
  const client = await new GenericContainer(image).withNetwork(network).start();
545
597
  opened.push(client);
@@ -620,26 +672,80 @@ const openLab = async (target, kind = "tcp") => {
620
672
  }
621
673
  return read;
622
674
  },
623
- async fault(kind, at = "target") {
675
+ async fault(kind, at = "target", options = {}) {
624
676
  const [where, peer] = at === "client" ? [admin, targetIp] : [injector, clientIp];
625
- const stamp = JSON.parse(await output(where, [
677
+ const args = [
626
678
  "/opt/harness/inject",
627
679
  kind,
628
- peer
629
- ]));
680
+ peer,
681
+ ...optionArgs(options)
682
+ ];
683
+ const stamp = JSON.parse(await output(where, args));
630
684
  return {
631
685
  t0: stamp.t0,
632
686
  t1: stamp.t1
633
687
  };
634
688
  },
635
- async lift(kind, at = "target") {
689
+ async lift(kind, at = "target", options = {}) {
636
690
  const [where, peer] = at === "client" ? [admin, targetIp] : [injector, clientIp];
637
691
  await output(where, [
638
692
  "/opt/harness/inject",
639
- kind === "blackhole" ? "clear" : "clear-rst",
640
- peer
693
+ LIFT[kind],
694
+ peer,
695
+ ...optionArgs(options)
641
696
  ]);
642
697
  },
698
+ async netem(spec, at = "target") {
699
+ const where = at === "client" ? admin : injector;
700
+ const args = spec === null ? [
701
+ "/opt/harness/inject",
702
+ "clear-netem",
703
+ "eth0"
704
+ ] : [
705
+ "/opt/harness/inject",
706
+ "netem",
707
+ "eth0",
708
+ ...spec.split(/\s+/)
709
+ ];
710
+ const stamp = JSON.parse(await output(where, args));
711
+ return {
712
+ t0: stamp.t0,
713
+ t1: stamp.t1
714
+ };
715
+ },
716
+ async offload(on, at = "target") {
717
+ await output(at === "client" ? admin : injector, [
718
+ "/opt/harness/inject",
719
+ "offload",
720
+ "eth0",
721
+ on ? "on" : "off"
722
+ ]);
723
+ },
724
+ async kill() {
725
+ if (target.kill === void 0) throw new Error(`${target.image} names no way to kill its server`);
726
+ await output(server, [...target.kill]);
727
+ },
728
+ async warm() {
729
+ await output(admin, [
730
+ "ping",
731
+ "-c",
732
+ "2",
733
+ "-W",
734
+ "2",
735
+ targetIp
736
+ ]);
737
+ await output(injector, [
738
+ "ping",
739
+ "-c",
740
+ "2",
741
+ "-W",
742
+ "2",
743
+ clientIp
744
+ ]);
745
+ },
746
+ async setup(commands) {
747
+ for (const argv of commands) await output(server, [...argv]);
748
+ },
643
749
  async heal(at, when) {
644
750
  const [where, peer] = at === "client" ? [admin, targetIp] : [injector, clientIp];
645
751
  const stamp = JSON.parse(await output(where, [
@@ -679,7 +785,8 @@ const openLab = async (target, kind = "tcp") => {
679
785
  go: () => output(container, ["touch", "/tmp/go"]).then(() => void 0),
680
786
  freeze: () => runtime.container.getById(container.getId()).pause().then(() => void 0),
681
787
  thaw: () => runtime.container.getById(container.getId()).unpause().then(() => void 0),
682
- stop: () => container.stop(STOP).then(() => void 0)
788
+ kill: () => runtime.container.getById(container.getId()).kill({ signal: "SIGKILL" }).then(() => void 0),
789
+ stop: () => container.stop(STOP).then(() => void 0).catch(() => void 0)
683
790
  };
684
791
  },
685
792
  async settle() {
@@ -765,6 +872,8 @@ const probeArgs = (cell, lab) => {
765
872
  probe.mode
766
873
  ];
767
874
  if (probe.request !== void 0) args.push("--request", probe.request);
875
+ if (probe.count !== void 0) args.push("--count", String(probe.count));
876
+ if (probe.interval !== void 0) args.push("--interval-ms", String(valueIn(probe.interval, "ms")));
768
877
  if (probe.driver !== void 0) {
769
878
  args.push("--keepalive-ms", String(valueIn(probe.driver.keepAlive, "ms")));
770
879
  args.push("--max-s", String(Math.ceil(longest * 2 + 10)));
@@ -777,13 +886,14 @@ const probeArgs = (cell, lab) => {
777
886
  };
778
887
  /**
779
888
  * This function waits for an error on the socket of the target for the connection of the
780
- * client. The prober records that error in the namespace of the target. A cell on the server side
781
- * waits for that. The promise resolves after `within` ms at the latest.
889
+ * client, or for its end of file (CLOSE_WAIT, state 8). The prober records both in the namespace
890
+ * of the target. A cell on the server side waits for that. The promise resolves after `within` ms
891
+ * at the latest.
782
892
  */
783
893
  const serverError = (lab, local, within) => new Promise((resolve) => {
784
894
  const started = Date.now();
785
895
  const look = () => {
786
- if (lab.kernel.some((record) => record.ev === "error" && (record.err ?? 0) !== 0 && record.netns === lab.netns.target && record.dst === local) || Date.now() - started > within) resolve();
896
+ if (lab.kernel.some((record) => record.netns === lab.netns.target && record.dst === local && (record.ev === "error" && (record.err ?? 0) !== 0 || record.ev === "state" && record.new === 8)) || Date.now() - started > within) resolve();
787
897
  else setTimeout(look, 50);
788
898
  };
789
899
  look();
@@ -813,6 +923,11 @@ const requestSent = (probe, within) => new Promise((resolve, reject) => {
813
923
  * next run. The run keeps the records of the prober from its start to its end. The frozen process
814
924
  * is the peer of the observer: the target, or the client for the server.
815
925
  */
926
+ /** This function gives the netem specification of a cell: its latency, then the netem of its fault when it has one. */
927
+ const netemSpec = (cell, fault) => {
928
+ const parts = [...cell.latency === void 0 ? [] : [`delay ${valueIn(cell.latency, "ms")}ms`], ...fault === void 0 ? [] : [fault]];
929
+ return parts.length === 0 ? null : parts.join(" ");
930
+ };
816
931
  const once = async (lab, cell, environment, index) => {
817
932
  await lab.forget();
818
933
  const from = lab.kernel.length;
@@ -834,17 +949,51 @@ const once = async (lab, cell, environment, index) => {
834
949
  let heal = null;
835
950
  try {
836
951
  await probe.until(["ready"], 15e3);
837
- const replyInFlight = server && cell.probe.mode === "inflight";
952
+ const replyInFlight = cell.probe.mode === "inflight" && (server && cell.fault !== "mtu-blackhole" || cell.fault === "fin");
838
953
  if (replyInFlight) {
839
954
  await go();
840
955
  await new Promise((resolve) => setTimeout(resolve, 200));
841
956
  }
842
957
  const faultAt = server ? "client" : "target";
843
- const network = cell.fault === "stall" ? "blackhole" : cell.fault;
844
- fault = network === "freeze-process" ? null : await lab.fault(network, faultAt);
845
- if (network === "freeze-process") await peer.freeze();
958
+ const direction = cell.drops === void 0 ? void 0 : cell.drops === "to-server" === (faultAt === "target") ? "in" : "out";
959
+ const options = {
960
+ ...direction === void 0 ? {} : { direction },
961
+ ...cell.icmp === void 0 ? {} : { code: cell.icmp },
962
+ ...cell.mtu === void 0 ? {} : { above: cell.mtu }
963
+ };
964
+ const apply = async () => {
965
+ switch (cell.fault) {
966
+ case "freeze-process":
967
+ await peer.freeze();
968
+ return null;
969
+ case "fin":
970
+ await (server ? probe.kill() : lab.kill());
971
+ return null;
972
+ case "unreachable": return lab.fault("reject", faultAt, { code: cell.icmp ?? "host" });
973
+ case "simplex": return lab.fault("blackhole", faultAt, options);
974
+ case "mtu-blackhole": return lab.fault("mtu", "client", { above: cell.mtu ?? 1200 });
975
+ case "netem": return lab.netem(netemSpec(cell, cell.netem), "target");
976
+ case "rst": return lab.fault("rst", faultAt);
977
+ default: return lab.fault("blackhole", faultAt);
978
+ }
979
+ };
980
+ const lift = async () => {
981
+ switch (cell.fault) {
982
+ case "freeze-process": return peer.thaw();
983
+ case "fin": return;
984
+ case "unreachable": return lab.lift("reject", faultAt, { code: cell.icmp ?? "host" });
985
+ case "simplex": return lab.lift("blackhole", faultAt, options);
986
+ case "mtu-blackhole": return lab.lift("mtu", "client", { above: cell.mtu ?? 1200 });
987
+ case "netem":
988
+ await lab.netem(netemSpec(cell, void 0), "target");
989
+ return;
990
+ case "rst": return lab.lift("rst", faultAt);
991
+ default: return heal === null ? lab.lift("blackhole", faultAt) : void 0;
992
+ }
993
+ };
994
+ fault = await apply();
846
995
  try {
847
- if (cell.probe.mode === "inflight" && !replyInFlight) await go();
996
+ if (cell.probe.mode !== "idle" && !replyInFlight) await go();
848
997
  const { prediction } = cell;
849
998
  if (cell.fault === "stall") {
850
999
  if (cell.stall === void 0) throw new Error(`${cell.name} says no stall's length`);
@@ -856,6 +1005,14 @@ const once = async (lab, cell, environment, index) => {
856
1005
  "error",
857
1006
  "eof"
858
1007
  ], window * 1e3).catch(() => void 0);
1008
+ } else if (prediction.detects === null) {
1009
+ const window = valueIn(prediction.window, "s") * 1e3;
1010
+ await probe.until([
1011
+ "done",
1012
+ "error",
1013
+ "eof",
1014
+ "deadline"
1015
+ ], window).catch(() => void 0);
859
1016
  } else if (prediction.detects) {
860
1017
  const within = valueIn(prediction.high, "s") * 2e3 + 1e4;
861
1018
  if (server) {
@@ -868,8 +1025,7 @@ const once = async (lab, cell, environment, index) => {
868
1025
  ], within).catch(() => void 0);
869
1026
  } else await new Promise((resolve) => setTimeout(resolve, valueIn(prediction.window, "s") * 1e3));
870
1027
  } finally {
871
- if (network === "freeze-process") await peer.thaw();
872
- else if (heal === null) await lab.lift(network, faultAt);
1028
+ await lift();
873
1029
  }
874
1030
  if (cell.fault === "freeze-process" && !server) await probe.until(["data"], 5e3).catch(() => void 0);
875
1031
  } finally {
@@ -897,6 +1053,14 @@ const inEnvironment = async (cell, environment, count, say) => {
897
1053
  try {
898
1054
  const settings = await lab.configure(cell.targetSettings ?? {});
899
1055
  const targetSysctls = await lab.targetSysctl(cell.targetSysctls ?? {});
1056
+ if (cell.targetSetup !== void 0) await lab.setup(cell.targetSetup);
1057
+ if (cell.fault === "mtu-blackhole" || cell.fault === "netem") {
1058
+ await lab.offload(false, "target");
1059
+ await lab.offload(false, "client");
1060
+ }
1061
+ const latency = netemSpec(cell, void 0);
1062
+ if (latency !== null) await lab.netem(latency, "target");
1063
+ await lab.warm();
900
1064
  const manifest = await lab.manifest(await lab.sysctl(cell.sysctls), settings, targetSysctls);
901
1065
  const runs = [];
902
1066
  for (let index = 0; index < count; index += 1) {