@mark1russell7/harness 0.3.0 → 0.4.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/build/index.js CHANGED
@@ -24,6 +24,55 @@ const STEPS = [
24
24
  ];
25
25
  const s = (value) => quantity(Math.round(value * 1e6) / 1e6, "s");
26
26
  const name = (run) => `env ${run.environment} run ${run.index}`;
27
+ /** This function gives the value at quantile p of sorted values, by nearest rank: x_(⌈p·n⌉). */
28
+ const quantileOf = (sorted, p) => sorted[Math.min(sorted.length, Math.max(1, Math.ceil(p * sorted.length))) - 1];
29
+ /** This function gives the spread of a set of times, or null for none. */
30
+ const spreadOf = (times) => {
31
+ if (times.length === 0) return null;
32
+ const sorted = times.map((time) => valueIn(time, "s")).sort((a, b) => a - b);
33
+ return {
34
+ count: sorted.length,
35
+ median: s(middle(sorted)),
36
+ p99: s(quantileOf(sorted, .99)),
37
+ p999: s(quantileOf(sorted, .999)),
38
+ max: s(sorted.at(-1))
39
+ };
40
+ };
41
+ /** This function adds the reordering records of runs: the counts add, and each largest value is the largest of all. */
42
+ const reorderingTotal = (runs) => {
43
+ const each = runs.flatMap((run) => run.reordering === void 0 ? [] : [run.reordering]);
44
+ if (each.length === 0) return void 0;
45
+ const sum = (key) => each.reduce((total, one) => total + one[key], 0);
46
+ const add = (key) => each.some((one) => one[key] !== null) ? each.reduce((total, one) => total + (one[key] ?? 0), 0) : null;
47
+ const top = (key) => each.some((one) => one[key] !== null) ? Math.max(...each.map((one) => one[key] ?? 0)) : null;
48
+ return {
49
+ outOfOrder: sum("outOfOrder"),
50
+ duplicates: sum("duplicates"),
51
+ ooo: add("ooo"),
52
+ sacked: top("sacked"),
53
+ lost: top("lost"),
54
+ dsacks: add("dsacks"),
55
+ fast: sum("fast"),
56
+ degree: top("degree"),
57
+ seen: add("seen")
58
+ };
59
+ };
60
+ /** This function says a reordering record in words. */
61
+ const reorderingWords = (total) => [
62
+ `${total.outOfOrder} arrivals out of order and ${total.duplicates} duplicate arrivals at the client`,
63
+ ...total.ooo === null ? [] : [`its kernel counted ${total.ooo} segments out of order`],
64
+ ...total.sacked === null ? [] : [`the server held up to ${total.sacked} segments SACKed`],
65
+ ...total.dsacks === null ? [] : [`got ${total.dsacks} DSACKs`],
66
+ `sent ${total.fast} fast retransmissions`,
67
+ ...total.seen === null ? [] : [`saw ${total.seen} reorderings`],
68
+ ...total.degree === null ? [] : [`and its reordering degree went to ${total.degree}`]
69
+ ].join(", ");
70
+ /** This function says a spread in words, in milliseconds. */
71
+ const spreadWords = (spread) => {
72
+ if (spread === null) return "no time";
73
+ const ms = (time) => Math.round(valueIn(time, "s") * 1e6) / 1e3;
74
+ return `median ${ms(spread.median)} ms, p99 ${ms(spread.p99)} ms, p99.9 ${ms(spread.p999)} ms, max ${ms(spread.max)} ms of ${spread.count}`;
75
+ };
27
76
  /** This function gives P(X ≤ j) for X ~ Binomial(n, 1/2), exactly. */
28
77
  const below = (n, j) => {
29
78
  let term = 1;
@@ -82,6 +131,25 @@ const verdictOf = (prediction, runs) => {
82
131
  why: "no run counted"
83
132
  };
84
133
  if (prediction.detects === null) {
134
+ const crossed = counted.flatMap((run) => run.crossings === void 0 ? [] : [run.crossings]);
135
+ if (crossed.length > 0) {
136
+ const hop = crossed[0].hop;
137
+ const up = spreadOf(crossed.flatMap((each) => each.up));
138
+ const down = spreadOf(crossed.flatMap((each) => each.down));
139
+ return {
140
+ ...empty,
141
+ decision: "measured",
142
+ band: null,
143
+ outside: [],
144
+ median: up?.median ?? null,
145
+ proxy: {
146
+ hop,
147
+ up,
148
+ down
149
+ },
150
+ why: `${counted.length} runs measured ${hop}: toward the target ${spreadWords(up)}; toward the client ${spreadWords(down)}. ${prediction.why}`
151
+ };
152
+ }
85
153
  if (prediction.from !== void 0) {
86
154
  const times = counted.flatMap((run) => run.elapsed === null ? [] : [valueIn(run.elapsed, "s")]).sort((a, b) => a - b);
87
155
  const kinds = /* @__PURE__ */ new Map();
@@ -103,13 +171,24 @@ const verdictOf = (prediction, runs) => {
103
171
  const samples = counted.flatMap((run) => (run.samples ?? []).map((sample) => valueIn(sample, "s"))).sort((a, b) => a - b);
104
172
  const retransmissions = counted.reduce((sum, run) => sum + run.retransmissions, 0);
105
173
  const ended = counted.filter((run) => run.outcome.kind === "error" || run.outcome.kind === "eof");
106
- 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}`;
174
+ const reordering = reorderingTotal(counted);
175
+ const reordered = reordering !== void 0 && [
176
+ reordering.outOfOrder,
177
+ reordering.duplicates,
178
+ reordering.ooo,
179
+ reordering.sacked,
180
+ reordering.dsacks,
181
+ reordering.fast,
182
+ reordering.seen
183
+ ].some((count) => (count ?? 0) > 0) ? ` Reordering: ${reorderingWords(reordering)}.` : "";
184
+ 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, p99 ${s(quantileOf(samples, .99)).value} s, slowest ${s(samples.at(-1)).value} s, ${retransmissions} retransmissions, ${ended.length} connections ended.${reordered} ${prediction.why}`;
107
185
  return {
108
186
  ...empty,
109
187
  decision: "measured",
110
188
  band: null,
111
189
  outside: [],
112
190
  median: samples.length === 0 ? null : s(middle(samples)),
191
+ ...reordering === void 0 ? {} : { reordering },
113
192
  why
114
193
  };
115
194
  }
@@ -277,6 +356,65 @@ const peerOf = (cell, run) => {
277
356
  cookie
278
357
  };
279
358
  };
359
+ /** This function tells if sequence number a is after b, in the 32-bit space of TCP (`after` in include/net/tcp.h). */
360
+ const after32 = (a, b) => (a - b | 0) > 0;
361
+ /** This function gives the largest value of a count of the kernel in records, or null when no record has it. */
362
+ const most = (records, count) => records.reduce((top, record) => {
363
+ const value = count(record);
364
+ return value === void 0 ? top : Math.max(top ?? 0, value);
365
+ }, null);
366
+ /** This function gives what reordering did in a run, from the records of the receiver and of the sender. */
367
+ const reorderingOf = (receiver, sender) => {
368
+ const arrivals = receiver.filter((record) => record.ev === "rx" && (record.seg?.len.value ?? 0) > 0 && record.tcp?.rcv_nxt !== void 0);
369
+ return {
370
+ outOfOrder: arrivals.filter((record) => after32(record.seg.seq, record.tcp.rcv_nxt)).length,
371
+ duplicates: arrivals.filter((record) => !after32(record.seg.seq + record.seg.len.value, record.tcp.rcv_nxt)).length,
372
+ ooo: most(receiver, (record) => record.tcp?.rcv_ooopack),
373
+ sacked: most(sender, (record) => record.tcp?.sacked),
374
+ lost: most(sender, (record) => record.tcp?.lost),
375
+ dsacks: most(sender, (record) => record.tcp?.dsack_dups),
376
+ fast: sender.filter((record) => record.ev === "retrans" && record.why === "fast").length,
377
+ degree: most(sender, (record) => record.tcp?.reordering),
378
+ seen: most(sender, (record) => record.tcp?.reord_seen)
379
+ };
380
+ };
381
+ /**
382
+ * This function gives the times of a proxy in a run, for each request after the first. The time
383
+ * of a request starts at its arrival on the socket of the proxy toward the client. It stops at the
384
+ * next write on the socket toward the target. The time of its reply starts at its arrival on the
385
+ * socket toward the target, and it stops at the next write toward the client. A request without
386
+ * each record gives no time.
387
+ */
388
+ const crossingsOf = (run, hop, toClient, toTarget) => {
389
+ const up = [];
390
+ const down = [];
391
+ if (toClient === void 0 || toTarget === void 0) return {
392
+ hop,
393
+ up,
394
+ down
395
+ };
396
+ const of = (place) => run.kernel.filter((record) => record.netns === place.netns && record.cookie === place.cookie);
397
+ const near = of(toClient);
398
+ const far = of(toTarget);
399
+ const arrival = (records, from, to) => records.find((record) => record.ev === "rx" && (record.seg?.len.value ?? 0) > 0 && record.mono > from && record.mono < to);
400
+ const write = (records, from, to) => records.find((record) => record.ev === "tx" && record.mono >= from && record.mono < to);
401
+ const sends = run.events.filter((event) => event.ev === "sent").slice(1);
402
+ sends.forEach((send, n) => {
403
+ const next = sends[n + 1]?.t ?? Infinity;
404
+ const request = arrival(near, send.t, next);
405
+ const relayed = request === void 0 ? void 0 : write(far, request.mono, next);
406
+ if (request === void 0 || relayed === void 0) return;
407
+ up.push(between(request.mono, relayed.mono));
408
+ const reply = arrival(far, relayed.mono, next);
409
+ const answered = reply === void 0 ? void 0 : write(near, reply.mono, next);
410
+ if (reply !== void 0 && answered !== void 0) down.push(between(reply.mono, answered.mono));
411
+ });
412
+ return {
413
+ hop,
414
+ up,
415
+ down
416
+ };
417
+ };
280
418
  /**
281
419
  * This function reads one run against its cell, from the records of the kernel first.
282
420
  * - The socket of the run is the socket of its observer. It is the socket that the client connected
@@ -321,12 +459,15 @@ const measure = (cell, run) => {
321
459
  return firings.find((record) => timer === "keepalive" && record.owned === true) === void 0 ? null : "its keepalive timer found the socket held by its owner and put itself off by HZ/20: the run measured the owner, not the timer";
322
460
  };
323
461
  const acks = request === void 0 ? 0 : socket.filter((record) => record.ev === "rx" && record.mono > request.t).length;
462
+ const srtt = socket.find((record) => record.tcp?.srtt !== void 0)?.tcp?.srtt;
463
+ const rtt = srtt === void 0 ? null : quantity(valueIn(srtt, "us") / 1e3, "ms");
324
464
  const base = {
325
465
  environment: run.environment,
326
466
  index: run.index,
327
467
  probes: keepalives.length,
328
468
  retransmissions: retransmitted.length,
329
- acks
469
+ acks,
470
+ rtt
330
471
  };
331
472
  const nothing = {
332
473
  outcome: { kind: "silent" },
@@ -370,6 +511,8 @@ const measure = (cell, run) => {
370
511
  else if (replyFirst && replied) voided = "the server's reply arrived before the fault held";
371
512
  else if (keepalives[0] !== void 0 && keepalives[0].mono < run.fault.t1) voided = "a keepalive probe went out before the fault held";
372
513
  }
514
+ const assumed = cell.prediction.detects === null ? void 0 : cell.prediction.rtt;
515
+ if (voided === null && assumed !== void 0 && rtt !== null && valueIn(rtt, "ms") > valueIn(assumed.high, "ms")) voided = `rtt-over-assumption: its first round trip was ${rtt.value} ms, above the ${valueIn(assumed.high, "ms")} ms that the prediction assumes`;
373
516
  const remote = cell.observer !== void 0 && cell.observer !== "client";
374
517
  const reported = socket.find((record) => record.ev === "error" && (record.err ?? 0) !== 0);
375
518
  const seen = remote ? void 0 : events.find((event) => event.ev === "error" || event.ev === "eof");
@@ -397,13 +540,29 @@ const measure = (cell, run) => {
397
540
  };
398
541
  if (prediction.detects === null) {
399
542
  const sends = events.filter((event) => event.ev === "sent").slice(1);
543
+ const answers = events.some((event) => event.ev === "replied") ? "replied" : "data";
400
544
  const samples = sends.flatMap((send, n) => {
401
545
  const next = sends[n + 1]?.t ?? Infinity;
402
- const reply = events.find((event) => event.ev === "data" && event.t > send.t && event.t < next);
546
+ const reply = events.find((event) => event.ev === answers && event.t > send.t && event.t < next);
403
547
  return reply === void 0 ? [] : [between(send.t, reply.t)];
404
548
  });
405
549
  const from = prediction.from === void 0 ? void 0 : referenceAt(prediction.from);
406
550
  const until = ended ?? after.at(-1)?.t;
551
+ const peerRecords = peerCookie === void 0 ? [] : kernel.filter((record) => record.netns === peerNetns && record.cookie === peerCookie);
552
+ const proxy = cell.calibrates === void 0 ? void 0 : run.topology?.hops[cell.calibrates];
553
+ const crossings = cell.calibrates === void 0 ? {} : { crossings: crossingsOf(run, cell.calibrates, proxy === void 0 ? void 0 : socketOf({
554
+ ...cell,
555
+ observer: {
556
+ hop: cell.calibrates,
557
+ toward: "client"
558
+ }
559
+ }, run), proxy === void 0 ? void 0 : socketOf({
560
+ ...cell,
561
+ observer: {
562
+ hop: cell.calibrates,
563
+ toward: "target"
564
+ }
565
+ }, run)) };
407
566
  return {
408
567
  ...base,
409
568
  ...nothing,
@@ -413,6 +572,8 @@ const measure = (cell, run) => {
413
572
  noticed: prediction.from === void 0 ? null : noticed,
414
573
  steps: retransmitted.filter((record) => record.mono > request.t).map((record) => between(request.t, record.mono)),
415
574
  samples,
575
+ reordering: reorderingOf(socket.filter((record) => record.mono > request.t), peerRecords.filter((record) => record.mono > request.t)),
576
+ ...crossings,
416
577
  void: voided
417
578
  };
418
579
  }
@@ -794,6 +955,12 @@ const clientOf = async (kind, image) => {
794
955
  clientKind: kind
795
956
  };
796
957
  };
958
+ /** This function gives the device of a route from its line in `ip -o route get`: "10.0.0.2 dev eth0 src …". */
959
+ const deviceOf = (route) => {
960
+ const device = /\bdev (\S+)/.exec(route)?.[1];
961
+ if (device === void 0) throw new Error(`no device in the route: ${route}`);
962
+ return device;
963
+ };
797
964
  /**
798
965
  * This function opens one environment:
799
966
  * - a chain of new networks, and the stock target on the last one. A direct pair has one network.
@@ -871,10 +1038,12 @@ const openLab = async (target, kind = "tcp", topology) => {
871
1038
  await join(box, targetSide);
872
1039
  const ready = await seen.until(["mbx-ready"], 3e4, `router ${hop.name}`);
873
1040
  await seen.until(hop.form === "reset-both" ? ["resetter"] : ["ct-watch"], 3e4, `router ${hop.name}`);
1041
+ const injector = await new GenericContainer(image).withNetworkMode(`container:${box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW").start();
1042
+ opened.push(injector);
874
1043
  inChain[at] = {
875
1044
  hop,
876
1045
  box,
877
- injector: box,
1046
+ injector,
878
1047
  ip: {
879
1048
  client: await ipOn(box, clientSide),
880
1049
  target: await ipOn(box, targetSide)
@@ -883,6 +1052,10 @@ const openLab = async (target, kind = "tcp", topology) => {
883
1052
  client: "",
884
1053
  target: endpointOf(at + 1)
885
1054
  },
1055
+ devices: {
1056
+ client: "",
1057
+ target: ""
1058
+ },
886
1059
  netns: await netnsOf(box),
887
1060
  sysctls: { "net.netfilter.nf_conntrack_tcp_timeout_established": String(ready["idle"]) }
888
1061
  };
@@ -913,6 +1086,10 @@ const openLab = async (target, kind = "tcp", topology) => {
913
1086
  client: "",
914
1087
  target: next
915
1088
  },
1089
+ devices: {
1090
+ client: "",
1091
+ target: ""
1092
+ },
916
1093
  netns: await netnsOf(injector),
917
1094
  sysctls: {}
918
1095
  };
@@ -961,7 +1138,21 @@ const openLab = async (target, kind = "tcp", topology) => {
961
1138
  }
962
1139
  for (let at = 0; at < inChain.length; at += 1) {
963
1140
  const hop = inChain[at];
964
- if (hop.hop.kind !== "proxy" || hop.hop.sysctls === void 0) continue;
1141
+ const toward = async (address) => deviceOf(await output(hop.injector, [
1142
+ "ip",
1143
+ "-o",
1144
+ "route",
1145
+ "get",
1146
+ address.split(":")[0]
1147
+ ]));
1148
+ inChain[at] = {
1149
+ ...hop,
1150
+ devices: {
1151
+ client: await toward(hop.peer.client),
1152
+ target: await toward(hop.peer.target)
1153
+ }
1154
+ };
1155
+ if (hop.hop.sysctls === void 0) continue;
965
1156
  const where = await adminOf(at + 1);
966
1157
  const settings = Object.entries(hop.hop.sysctls).map(([key, value]) => `${key}=${value}`);
967
1158
  await output(where, [
@@ -1010,13 +1201,38 @@ const openLab = async (target, kind = "tcp", topology) => {
1010
1201
  if (found === void 0) throw new Error(`the topology has no hop ${name}`);
1011
1202
  return found;
1012
1203
  };
1013
- /** This function gives the namespace and the peer of the rules of a fault at a place. */
1204
+ /**
1205
+ * This function gives the namespace and the peer of the rules of a fault at a place, and the
1206
+ * other arguments of the injector. At a proxy, the rules are INPUT and OUTPUT rules, which act
1207
+ * on its sockets. A router forwards past INPUT and OUTPUT, thus its rules are FORWARD rules on
1208
+ * its device toward the side (`--via`).
1209
+ */
1014
1210
  const placeOf = (at) => {
1015
- if (at === "client") return [admin, targetIp];
1016
- if (at === "target") return [injector, targetPeer];
1211
+ if (at === "client") return [
1212
+ admin,
1213
+ targetIp,
1214
+ []
1215
+ ];
1216
+ if (at === "target") return [
1217
+ injector,
1218
+ targetPeer,
1219
+ []
1220
+ ];
1017
1221
  const hop = hopNamed(at.hop);
1018
- if (hop.hop.kind !== "proxy") throw new Error(`a fault at ${at.hop} needs a proxy: a router forwards past INPUT and OUTPUT`);
1019
- return [hop.injector, at.toward === "client" ? hop.peer.client : hop.peer.target.split(":")[0]];
1222
+ const peer = at.toward === "client" ? hop.peer.client : hop.peer.target.split(":")[0];
1223
+ const via = hop.hop.kind === "router" ? ["--via", hop.devices[at.toward]] : [];
1224
+ return [
1225
+ hop.injector,
1226
+ peer,
1227
+ via
1228
+ ];
1229
+ };
1230
+ /** This function gives the namespace and the device of netem at a place: an end, or the device of a hop toward a side. */
1231
+ const deviceAt = (at) => {
1232
+ if (at === "client") return [admin, "eth0"];
1233
+ if (at === "target") return [injector, "eth0"];
1234
+ const hop = hopNamed(at.hop);
1235
+ return [hop.injector, hop.devices[at.toward]];
1020
1236
  };
1021
1237
  const toxiproxy = () => {
1022
1238
  const found = inChain.find((hop) => hop.hop.kind === "proxy" && hop.hop.api !== void 0);
@@ -1090,42 +1306,46 @@ const openLab = async (target, kind = "tcp", topology) => {
1090
1306
  return read;
1091
1307
  },
1092
1308
  async fault(kind, at = "target", options = {}) {
1093
- const [where, peer] = placeOf(at);
1309
+ const [where, peer, via] = placeOf(at);
1094
1310
  return stamped(where, [
1095
1311
  "/opt/harness/inject",
1096
1312
  kind,
1097
1313
  peer,
1098
- ...optionArgs(options)
1314
+ ...optionArgs(options),
1315
+ ...via
1099
1316
  ]);
1100
1317
  },
1101
1318
  async lift(kind, at = "target", options = {}) {
1102
- const [where, peer] = placeOf(at);
1319
+ const [where, peer, via] = placeOf(at);
1103
1320
  await output(where, [
1104
1321
  "/opt/harness/inject",
1105
1322
  LIFT[kind],
1106
1323
  peer,
1107
- ...optionArgs(options)
1324
+ ...optionArgs(options),
1325
+ ...via
1108
1326
  ]);
1109
1327
  },
1110
1328
  async netem(spec, at = "target") {
1111
- const where = at === "client" ? admin : injector;
1329
+ const [where, device] = deviceAt(at);
1112
1330
  const args = spec === null ? [
1113
1331
  "/opt/harness/inject",
1114
1332
  "clear-netem",
1115
- "eth0"
1333
+ device
1116
1334
  ] : [
1117
1335
  "/opt/harness/inject",
1118
1336
  "netem",
1119
- "eth0",
1337
+ device,
1120
1338
  ...spec.split(/\s+/)
1121
1339
  ];
1122
1340
  return stamped(where, args);
1123
1341
  },
1124
1342
  async offload(on, at = "target") {
1125
- await output(at === "client" ? admin : injector, [
1343
+ const devices = typeof at === "object" ? Object.values(hopNamed(at.hop).devices) : [deviceAt(at)[1]];
1344
+ const [where] = deviceAt(at);
1345
+ for (const device of devices) await output(where, [
1126
1346
  "/opt/harness/inject",
1127
1347
  "offload",
1128
- "eth0",
1348
+ device,
1129
1349
  on ? "on" : "off"
1130
1350
  ]);
1131
1351
  },
@@ -1199,13 +1419,14 @@ const openLab = async (target, kind = "tcp", topology) => {
1199
1419
  for (const argv of commands) await output(server, [...argv]);
1200
1420
  },
1201
1421
  async heal(at, when) {
1202
- const [where, peer] = placeOf(at);
1422
+ const [where, peer, via] = placeOf(at);
1203
1423
  return stamped(where, [
1204
1424
  "/opt/harness/inject",
1205
1425
  "clear",
1206
1426
  peer,
1207
1427
  "--at",
1208
- String(Math.round(when))
1428
+ String(Math.round(when)),
1429
+ ...via
1209
1430
  ]);
1210
1431
  },
1211
1432
  async forget() {
@@ -1469,6 +1690,8 @@ const once = async (lab, cell, environment, index, dry = false) => {
1469
1690
  };
1470
1691
  const toxicFirst = cell.toxic !== void 0 && cell.fault !== "toxic";
1471
1692
  if (toxicFirst) await lab.toxic(cell.toxic);
1693
+ const netemAt = cell.faultAt ?? "target";
1694
+ const netemOn = (fault) => netemAt === "target" ? lab.netem(netemSpec(cell, fault), "target") : lab.netem(fault ?? null, netemAt);
1472
1695
  const apply = async () => {
1473
1696
  switch (cell.fault) {
1474
1697
  case "freeze-process":
@@ -1480,10 +1703,11 @@ const once = async (lab, cell, environment, index, dry = false) => {
1480
1703
  case "unreachable": return lab.fault("reject", faultAt, { code: cell.icmp ?? "host" });
1481
1704
  case "simplex": return lab.fault("blackhole", faultAt, options);
1482
1705
  case "mtu-blackhole": return lab.fault("mtu", "client", { above: cell.mtu ?? 1200 });
1483
- case "netem": return lab.netem(netemSpec(cell, cell.netem), "target");
1706
+ case "netem": return netemOn(cell.netem);
1484
1707
  case "rst": return lab.fault("rst", faultAt);
1485
1708
  case "middlebox-forget":
1486
- case "proxy-idle": return null;
1709
+ case "proxy-idle":
1710
+ case "none": return null;
1487
1711
  case "toxic":
1488
1712
  if (cell.toxic === void 0) throw new Error(`${cell.name} names no toxic`);
1489
1713
  return lab.toxic(cell.toxic);
@@ -1499,12 +1723,13 @@ const once = async (lab, cell, environment, index, dry = false) => {
1499
1723
  case "fin":
1500
1724
  case "middlebox-forget":
1501
1725
  case "proxy-idle":
1502
- case "server-close": return;
1726
+ case "server-close":
1727
+ case "none": return;
1503
1728
  case "unreachable": return lab.lift("reject", faultAt, { code: cell.icmp ?? "host" });
1504
1729
  case "simplex": return lab.lift("blackhole", faultAt, options);
1505
1730
  case "mtu-blackhole": return lab.lift("mtu", "client", { above: cell.mtu ?? 1200 });
1506
1731
  case "netem":
1507
- await lab.netem(netemSpec(cell, void 0), "target");
1732
+ await netemOn(void 0);
1508
1733
  return;
1509
1734
  case "rst": return lab.lift("rst", faultAt);
1510
1735
  case "toxic":
@@ -1591,6 +1816,7 @@ const inEnvironment = async (cell, environment, count, say) => {
1591
1816
  if (cell.fault === "mtu-blackhole" || cell.fault === "netem") {
1592
1817
  await lab.offload(false, "target");
1593
1818
  await lab.offload(false, "client");
1819
+ if (typeof cell.faultAt === "object") await lab.offload(false, cell.faultAt);
1594
1820
  }
1595
1821
  const latency = netemSpec(cell, void 0);
1596
1822
  if (latency !== null) await lab.netem(latency, "target");
@@ -1665,6 +1891,6 @@ const runCell = async (cell, say = () => {}) => {
1665
1891
  }
1666
1892
  };
1667
1893
  //#endregion
1668
- export { LISTING, NOISE, PUMP, RELATIVE, STEPS, endpointAfter, harnessImage, judge, measure, medianInterval, nodeClientImage, openLab, routerImage, routesOf, runCell, socketOf, spread, templated, toxicBody, verdictOf };
1894
+ export { LISTING, NOISE, PUMP, RELATIVE, STEPS, crossingsOf, deviceOf, endpointAfter, harnessImage, judge, measure, medianInterval, nodeClientImage, openLab, quantileOf, reorderingOf, routerImage, routesOf, runCell, socketOf, spread, spreadOf, templated, toxicBody, verdictOf };
1669
1895
 
1670
1896
  //# sourceMappingURL=index.js.map