@mark1russell7/kind 0.1.2 → 0.1.4

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/image/agent.mjs CHANGED
@@ -2,15 +2,24 @@
2
2
  // container, thus it sees the processes of each pod of the node, and PID 1 is the init of the node.
3
3
  //
4
4
  // node agent.mjs serve polls the rules of kube-proxy in the namespaces of the node, as
5
- // /tmp/rules.json asks, and prints each change of the backends
5
+ // /tmp/rules.json asks, and prints each change of the backends. It
6
+ // also reads the conntrack table of the node (procfs, or netlink
7
+ // with the conntrack tool of the node). It prints each entry
8
+ // that keeps its NAT to a pod that the rules dropped (`stale`), each
9
+ // handshake that stopped for a second (`stalled`). Each line
10
+ // `rules-alive` also counts the reads of the table (`ct`). Each
11
+ // second it reads the counters of dropped packets of the node and
12
+ // of each client pod, and prints each one that grew (`drops`)
6
13
  // node agent.mjs act JSON does one act and prints its stamps on CLOCK_MONOTONIC
7
14
  //
8
15
  // The acts: signal (a signal to a PID), pause (SIGSTOP, then SIGCONT after ms), write (a file, for
9
16
  // example cpu.max or a flag file in the root of a container), remove (a file), stall (the stall
10
17
  // seam of the app: the ms in /tmp/lab-stall, then SIGUSR2), and now.
11
18
  import { spawnSync } from "node:child_process";
12
- import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
19
+ import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
13
20
  import { backendsOf } from "./backends.ts";
21
+ import { entriesOf, flowsOf, staleOf, stalledOf } from "./conntrack.ts";
22
+ import { devOf, grownOf, snmpOf, softnetOf, unresolvedOf } from "./drops.ts";
14
23
 
15
24
  const now = () => Number(process.hrtime.bigint()) / 1e6;
16
25
  const node = process.env.NODE_NAME ?? "";
@@ -24,6 +33,115 @@ const DUMP = {
24
33
  ipvs: ["nsenter", "-t", "1", "-n", "--", "cat", "/proc/net/ip_vs"],
25
34
  };
26
35
 
36
+ /**
37
+ * the TCP lines of the conntrack table of the node's network namespace: those of the ClusterIP and
38
+ * those of a handshake. A kernel without CONFIG_NF_CONNTRACK_PROCFS has no /proc/net/nf_conntrack
39
+ * (the kernel of GitHub's runner, 6.17), thus the agent then asks netlink with the conntrack tool of
40
+ * the node. Both give the same form of line. The first read decides the source. `text` is null when
41
+ * no source answers, and `why` tells why.
42
+ *
43
+ * A dump through netlink walks each bucket of the table of the whole kernel with the softirqs off,
44
+ * whatever its filter. A segment that comes in that walk waits for its end. Thus each read is one
45
+ * dump of TCP, and the agent filters it. The workflow makes the table small (kind.yml)
46
+ */
47
+ const SOURCES = {
48
+ procfs: (ip) => [["nsenter", "-t", "1", "-n", "--", "grep", "-F", "-e", ` dst=${ip} `, "-e", "[UNREPLIED]", "-e", " SYN_RECV ", "/proc/net/nf_conntrack"]],
49
+ netlink: () => [["nsenter", "-t", "1", "-m", "-n", "--", "conntrack", "-L", "-p", "tcp"]],
50
+ };
51
+ let source = null;
52
+ const conntrackOf = (ip) => {
53
+ const why = [];
54
+ for (const name of source === null ? Object.keys(SOURCES) : [source]) {
55
+ const lines = new Set();
56
+ let ok = true;
57
+ for (const [command, ...args] of SOURCES[name](ip)) {
58
+ const run = spawnSync(command, args, { encoding: "utf8", maxBuffer: 64 << 20 });
59
+ // grep exits 1 when no line matches; conntrack exits 0 with no entry
60
+ if (run.status === 0 || (name === "procfs" && run.status === 1)) {
61
+ for (const line of (run.stdout ?? "").split("\n")) if (line.trim() !== "") lines.add(line);
62
+ } else {
63
+ ok = false;
64
+ why.push(`${name}: ${(run.stderr ?? "").trim().slice(0, 200) || `exit ${run.status ?? run.signal ?? run.error?.message}`}`);
65
+ break;
66
+ }
67
+ }
68
+ if (ok) {
69
+ source = name;
70
+ return { text: [...lines].join("\n"), why: null };
71
+ }
72
+ }
73
+ return { text: null, why: why.join("; ") };
74
+ };
75
+ /** the PIDs of the clients of the node: the agent is in the PID namespace of the node, thus /proc holds each pod */
76
+ const clientsOf = () => {
77
+ const out = [];
78
+ for (const name of readdirSync("/proc")) {
79
+ if (!/^\d+$/.test(name)) continue;
80
+ try {
81
+ if (readFileSync(`/proc/${name}/cmdline`, "utf8").includes("client.mjs")) out.push(name);
82
+ } catch {
83
+ // the process ended in the scan
84
+ }
85
+ }
86
+ return out;
87
+ };
88
+
89
+ /**
90
+ * the counters of dropped packets, by name. For the node: the drops of the backlogs and the squeezes
91
+ * of the softirqs (the kernel, all CPUs), and the drops of each veth. For each client pod, through
92
+ * /proc/<pid>/net: the IP discards and the sends with no route, the failed connects, the drops of
93
+ * each interface, and the neighbors with no address yet
94
+ */
95
+ const countersOf = () => {
96
+ const values = {};
97
+ const read = (path) => {
98
+ try {
99
+ return readFileSync(path, "utf8");
100
+ } catch {
101
+ return null;
102
+ }
103
+ };
104
+ const softnet = read("/proc/1/net/softnet_stat");
105
+ if (softnet !== null) {
106
+ const { dropped, squeezed } = softnetOf(softnet);
107
+ values["kernel.backlog-drops"] = dropped;
108
+ values["kernel.squeezes"] = squeezed;
109
+ }
110
+ const dev = read("/proc/1/net/dev");
111
+ if (dev !== null) {
112
+ for (const [name, drops] of Object.entries(devOf(dev))) {
113
+ if (!name.startsWith("veth")) continue;
114
+ values[`node.${name}.rx-drops`] = drops.rx;
115
+ values[`node.${name}.tx-drops`] = drops.tx;
116
+ }
117
+ }
118
+ for (const pid of clientsOf()) {
119
+ const snmp = read(`/proc/${pid}/net/snmp`);
120
+ const own = read(`/proc/${pid}/net/dev`);
121
+ const arp = read(`/proc/${pid}/net/arp`);
122
+ if (snmp === null || own === null || arp === null) continue;
123
+ const ip = snmpOf(snmp, "Ip");
124
+ const tcp = snmpOf(snmp, "Tcp");
125
+ values[`client.${pid}.ip-out-discards`] = ip["OutDiscards"] ?? 0;
126
+ values[`client.${pid}.ip-out-no-routes`] = ip["OutNoRoutes"] ?? 0;
127
+ values[`client.${pid}.tcp-attempt-fails`] = tcp["AttemptFails"] ?? 0;
128
+ for (const [name, drops] of Object.entries(devOf(own))) {
129
+ if (name === "lo") continue;
130
+ values[`client.${pid}.${name}.rx-drops`] = drops.rx;
131
+ values[`client.${pid}.${name}.tx-drops`] = drops.tx;
132
+ }
133
+ values[`client.${pid}.unresolved-neighbors`] = unresolvedOf(arp);
134
+ }
135
+ return values;
136
+ };
137
+
138
+ /**
139
+ * how often the agent reads the conntrack table, in ms. Three dumps each 250 ms on each node took
140
+ * three quarters of the CPUs of a runner (run 39066066532), and the lab waited for an idle VM until
141
+ * the end. An entry of a SYN with no reply lives 120 s, thus a read each second sees it
142
+ */
143
+ const CONNTRACK_EVERY = 1000;
144
+
27
145
  const serve = async () => {
28
146
  let last = null;
29
147
  let previous = null;
@@ -31,6 +149,15 @@ const serve = async () => {
31
149
  let spent = 0;
32
150
  let beat = now();
33
151
  let dumped = null;
152
+ // the conntrack read: its last time, the stale entries and the stalled handshakes that it saw,
153
+ // whether the table exists, and the count of the reads since the last beat
154
+ let read = 0;
155
+ let seen = new Map();
156
+ let stuck = new Map();
157
+ let conntrack = true;
158
+ let count = { reads: 0, ms: 0, lines: 0, vip: 0, states: {}, syn: [], error: null };
159
+ // the counters of dropped packets at the last read, and the count of that last read's values
160
+ let counters = null;
34
161
  for (;;) {
35
162
  if (!existsSync("/tmp/rules.json")) {
36
163
  last = null;
@@ -58,10 +185,64 @@ const serve = async () => {
58
185
  say({ ev: "rules", t, prev: previous, took: Math.round(took * 1000) / 1000, b: backends });
59
186
  last = key;
60
187
  }
188
+ // a new SYN on the port of a stale entry goes to the pod of the entry, not to a backend of the rules
189
+ if (conntrack && t - read >= CONNTRACK_EVERY) {
190
+ read = t;
191
+ try {
192
+ const { text, why } = conntrackOf(rule.ip);
193
+ count.reads += 1;
194
+ count.ms += now() - t;
195
+ if (text === null) {
196
+ // the lab drops the lines before a run, thus each beat tells it again in ct.error
197
+ say({ ev: "conntrack-fail", t, why });
198
+ count.error = why;
199
+ conntrack = false;
200
+ } else {
201
+ const flows = flowsOf(text);
202
+ const entries = entriesOf(text, rule.ip, rule.port);
203
+ count.lines = flows.length;
204
+ count.vip = entries.length;
205
+ count.states = {};
206
+ for (const entry of entries) count.states[entry.state] = (count.states[entry.state] ?? 0) + 1;
207
+ // a sample of the entries in SYN_SENT: a fresh SYN has the life 120, and an old one less
208
+ count.syn = entries.filter((entry) => entry.state === "SYN_SENT").slice(0, 3).map((entry) => ({ client: entry.client, backend: entry.backend, ttl: entry.ttl, unreplied: entry.unreplied }));
209
+ const next = new Map();
210
+ for (const entry of staleOf(entries, backends)) {
211
+ const id = `${entry.client}>${entry.backend}`;
212
+ next.set(id, entry.state);
213
+ // each entry once, and again when its state changes
214
+ if (seen.get(id) !== entry.state) say({ ev: "stale", t, state: entry.state, ttl: entry.ttl, client: entry.client, backend: entry.backend, unreplied: entry.unreplied });
215
+ }
216
+ seen = next;
217
+ // a connection that times out: where its SYN went, and whether an answer came on this node
218
+ const held = new Map();
219
+ for (const flow of stalledOf(flows, 1)) {
220
+ const id = `${flow.from}>${flow.to}`;
221
+ held.set(id, flow.state);
222
+ if (stuck.get(id) !== flow.state) say({ ev: "stalled", t, state: flow.state, ttl: flow.ttl, from: flow.from, to: flow.to, reply: flow.reply, unreplied: flow.unreplied });
223
+ }
224
+ stuck = held;
225
+ }
226
+ } catch (error) {
227
+ // a read that fails stops the reads, and the next beat tells why
228
+ count.error = String(error?.stack ?? error).slice(0, 600);
229
+ conntrack = false;
230
+ }
231
+ }
61
232
  }
62
233
  previous = t;
63
234
  if (t - beat >= 1000) {
64
- say({ ev: "rules-alive", t, polls, ms: Math.round((spent / polls) * 100) / 100 });
235
+ // a packet lost between a client and the conntrack of the node leaves a trace in one of these
236
+ const next = countersOf();
237
+ if (counters !== null) {
238
+ const grown = grownOf(counters, next);
239
+ if (Object.keys(grown).length > 0) say({ ev: "drops", t, grown });
240
+ }
241
+ counters = next;
242
+ // the reads of the conntrack table since the last beat: a beat without them did not read it
243
+ const ct = { source, reads: count.reads, ms: count.reads === 0 ? 0 : Math.round((count.ms / count.reads) * 100) / 100, lines: count.lines, vip: count.vip, states: count.states, syn: count.syn, on: conntrack, ...(count.error === null ? {} : { error: count.error }) };
244
+ say({ ev: "rules-alive", t, polls, ms: Math.round((spent / polls) * 100) / 100, ct });
245
+ count = { ...count, reads: 0, ms: 0 };
65
246
  beat = t;
66
247
  polls = 0;
67
248
  spent = 0;
package/image/client.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // A client of a kind cell: an open loop of requests to the ClusterIP of the service, each on a new
2
2
  // connection, thus kube-proxy picks a backend for each one. Each request is one JSON line with its
3
- // start on CLOCK_MONOTONIC, its duration and its outcome: ok, 503, refused, reset, timeout,
4
- // unreachable, or another error.
3
+ // start on CLOCK_MONOTONIC, its duration, its outcome (ok, 503, refused, reset, timeout,
4
+ // unreachable, or another error) and its local port (`lp`).
5
5
  import http from "node:http";
6
6
 
7
7
  const [host, port] = (process.env.TARGET ?? "").split(":");
@@ -29,16 +29,27 @@ const outcomeOf = (error) => {
29
29
  return "error";
30
30
  };
31
31
 
32
+ // the local port of a socket, or null once its handle is closed. The kernel gives the port at
33
+ // connect(), and a refusal closes the handle in the poll phase of the loop: before setImmediate
34
+ const portOf = (socket) => {
35
+ const out = {};
36
+ socket?._handle?.getsockname?.(out);
37
+ return typeof out.port === "number" ? out.port : null;
38
+ };
39
+
32
40
  const fire = () => {
33
41
  const t = now();
34
42
  let done = false;
35
- // the local port of the connection: a failed request names it, to find a reuse of the port across connections
43
+ // the local port of the connection, on each request: a conntrack entry is per port, thus a failure
44
+ // on a port shows the connection before it on the same port, and the backend that it went to
36
45
  let lp = null;
46
+ let socket = null;
37
47
  const finish = (o, extra = {}) => {
38
48
  if (done) return;
39
49
  done = true;
40
50
  clearTimeout(timer);
41
- say({ ev: "req", t, d: Math.round((now() - t) * 1000) / 1000, o, ...extra, ...(o === "ok" ? {} : { lp }) });
51
+ lp ??= portOf(socket);
52
+ say({ ev: "req", t, d: Math.round((now() - t) * 1000) / 1000, o, ...extra, lp });
42
53
  };
43
54
  const req = http.request({ host, port: Number(port), path: "/work", agent: false, headers: { connection: "close" } }, (res) => {
44
55
  let body = "";
@@ -50,15 +61,20 @@ const fire = () => {
50
61
  res.on("error", (error) => finish(outcomeOf(error), { e: error.code ?? String(error) }));
51
62
  });
52
63
  const timer = setTimeout(() => {
64
+ // before the destroy closes the handle
65
+ lp ??= portOf(socket);
53
66
  const error = Object.assign(new Error("no answer in time"), { code: "LAB_TIMEOUT" });
54
67
  req.destroy(error);
55
68
  finish("timeout", { e: "LAB_TIMEOUT" });
56
69
  }, timeout);
57
70
  req.on("error", (error) => finish(outcomeOf(error), { e: error.code ?? String(error) }));
58
- req.on("socket", (socket) => {
59
- // the kernel gives the port at connect(), which comes before the next turn of the loop
60
- setImmediate(() => {
61
- lp = socket.localPort ?? null;
71
+ req.on("socket", (opened) => {
72
+ socket = opened;
73
+ // connect() comes in the tick before this event, thus the handle has the port here, a refused
74
+ // connection's too: in 200 refusals, each port was here and none at setImmediate
75
+ lp = portOf(opened);
76
+ opened.once("connect", () => {
77
+ lp ??= portOf(opened);
62
78
  });
63
79
  });
64
80
  req.end();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/kind",
4
- "version": "0.1.2",
4
+ "version": "0.1.4",
5
5
  "description": "Kubernetes in Docker as a lab for connection timers: a kind cluster from a scenario, faults in the namespaces of its pods, observations on the one monotonic clock of the VM, and predictions from the ordering law. Its judge entry reads and judges a raw record without Docker.",
6
6
  "keywords": [
7
7
  "kubernetes",
@@ -51,6 +51,8 @@
51
51
  "image/watch.mjs",
52
52
  "src/compose.ts",
53
53
  "src/backends.ts",
54
+ "src/conntrack.ts",
55
+ "src/drops.ts",
54
56
  "README.md",
55
57
  "LICENSE"
56
58
  ],
@@ -58,7 +60,7 @@
58
60
  "node": ">=22.19"
59
61
  },
60
62
  "dependencies": {
61
- "@mark1russell7/harness": "^0.11.0",
63
+ "@mark1russell7/harness": "^0.11.1",
62
64
  "@mark1russell7/quantity": "^0.2.0",
63
65
  "@mark1russell7/service": "^0.4.1",
64
66
  "testcontainers": "12.2.0"
@@ -0,0 +1,99 @@
1
+ // The parser of the conntrack table of a node. The agent of each node reads the table with it, and
2
+ // the image copies this file, thus it imports nothing.
3
+
4
+ /** A flow is one conntrack entry of TCP: its state, its life in seconds, its original tuple and the source of its replies. */
5
+ export interface ConntrackFlow {
6
+ readonly state: string;
7
+ readonly ttl: number;
8
+ /** This address is the original source of the flow, as `ip:port`. */
9
+ readonly from: string;
10
+ /** This address is the original destination of the flow, as `ip:port`: a ClusterIP before its DNAT. */
11
+ readonly to: string;
12
+ /** This address is the source of the replies, as `ip:port`: the backend that the DNAT of the entry chose. */
13
+ readonly reply: string;
14
+ /** This flag tells that no packet came back on the flow. */
15
+ readonly unreplied: boolean;
16
+ }
17
+
18
+ /** An entry is one conntrack entry of a TCP flow to the ClusterIP. It has a state, a life in seconds, a client and the backend of its NAT. */
19
+ export interface ConntrackEntry {
20
+ readonly state: string;
21
+ readonly ttl: number;
22
+ /** This address is the original source of the flow: the client, as `ip:port`. */
23
+ readonly client: string;
24
+ /** This address is the source of the replies: the backend that the DNAT of the entry chose, as `ip:port`. */
25
+ readonly backend: string;
26
+ /** This flag tells that no packet came back on the flow. */
27
+ readonly unreplied: boolean;
28
+ }
29
+
30
+ /**
31
+ * These states keep the NAT of an entry for a new SYN on the same port (nf_conntrack_proto_tcp.c at
32
+ * v6.8). A SYN on an entry in TIME_WAIT removes the entry and looks up again (:1011-1027), thus that
33
+ * SYN gets a backend from the rules. In each other state, the SYN goes to the backend of the entry.
34
+ * A CLOSE that a reset from the server made does not reopen either.
35
+ */
36
+ export const KEEPS_NAT: ReadonlySet<string> = new Set(["SYN_SENT", "SYN_SENT2", "SYN_RECV", "ESTABLISHED", "FIN_WAIT", "CLOSE_WAIT", "LAST_ACK", "CLOSE"]);
37
+
38
+ /**
39
+ * These are the lives of an entry in the states of a handshake, in seconds
40
+ * (nf_conntrack_tcp_timeout_syn_sent and _syn_recv). A retransmission of the SYN does not renew the
41
+ * life of SYN_SENT (nf_conntrack_proto_tcp.c at v6.8). Thus the life of such an entry tells the age
42
+ * of its first SYN.
43
+ */
44
+ export const HANDSHAKE_LIFE: Readonly<Record<string, number>> = { SYN_SENT: 120, SYN_RECV: 60 };
45
+
46
+ /** This form is one TCP line of `/proc/net/nf_conntrack` or `conntrack -L`. It has the timeout and state, the original tuple, the flags and counters, then the reply tuple. */
47
+ const LINE = /\btcp\s+\d+\s+(\d+)\s+(\S+)\s+src=(\S+) dst=(\S+) sport=(\d+) dport=(\d+)(.*?)\bsrc=(\S+) dst=\S+ sport=(\d+) dport=\d+/;
48
+
49
+ /** This function gives the TCP flows of a dump of the conntrack table. */
50
+ export const flowsOf = (text: string): ConntrackFlow[] => {
51
+ const out: ConntrackFlow[] = [];
52
+ for (const line of text.split("\n")) {
53
+ const match = LINE.exec(line);
54
+ if (match === null) continue;
55
+ out.push({
56
+ ttl: Number(match[1]),
57
+ state: match[2]!,
58
+ from: `${match[3]}:${match[5]}`,
59
+ to: `${match[4]}:${match[6]}`,
60
+ reply: `${match[8]}:${match[9]}`,
61
+ unreplied: match[7]!.includes("[UNREPLIED]"),
62
+ });
63
+ }
64
+ return out;
65
+ };
66
+
67
+ /** This function gives the TCP entries of a dump of the conntrack table whose original destination is the ClusterIP and port. */
68
+ export const entriesOf = (text: string, ip: string, port: number): ConntrackEntry[] =>
69
+ flowsOf(text)
70
+ .filter((flow) => flow.to === `${ip}:${port}`)
71
+ .map((flow) => ({ state: flow.state, ttl: flow.ttl, client: flow.from, backend: flow.reply, unreplied: flow.unreplied }));
72
+
73
+ /**
74
+ * This function gives the entries that keep their NAT to a pod that is not a backend of the rules.
75
+ * A new SYN from the client port of such an entry goes to that pod, whatever the rules say.
76
+ */
77
+ export const staleOf = (entries: readonly ConntrackEntry[], backends: readonly string[]): ConntrackEntry[] =>
78
+ entries.filter((entry) => KEEPS_NAT.has(entry.state) && !backends.includes(entry.backend));
79
+
80
+ /** This function gives the count of entries that `conntrack -D` deleted, from its last line: "N flow entries have been deleted". */
81
+ export const deletedOf = (text: string): number => {
82
+ const match = /(\d+) flow entr(?:y|ies) ha(?:s|ve) been deleted/.exec(text);
83
+ if (match === null) throw new Error(`conntrack -D gave no count: ${text.trim().slice(0, 200)}`);
84
+ return Number(match[1]);
85
+ };
86
+
87
+ /**
88
+ * This function gives the flows whose handshake stopped `age` seconds ago or more. Such a flow is
89
+ * a SYN with no reply (SYN_SENT), or a SYN and its SYN-ACK with no ACK (SYN_RECV). It is a
90
+ * connection that times out. On the node of the client, its reply address is the backend of its
91
+ * DNAT. On the node of that backend, a SYN_SENT tells that the backend did not answer. There, a
92
+ * SYN_RECV tells that its answer did not come back.
93
+ */
94
+ export const stalledOf = (flows: readonly ConntrackFlow[], age: number): ConntrackFlow[] =>
95
+ flows.filter((flow) => {
96
+ const life = HANDSHAKE_LIFE[flow.state];
97
+ if (life === undefined || (flow.state === "SYN_SENT" && !flow.unreplied)) return false;
98
+ return flow.ttl <= life - age;
99
+ });
package/src/drops.ts ADDED
@@ -0,0 +1,63 @@
1
+ // The counters of the dropped packets of a node and of its client pods. The agent of each node
2
+ // reads them each second, and the image copies this file, thus it imports nothing.
3
+
4
+ /** This function gives the values of one protocol of `/proc/net/snmp`: a line of names, then a line of values. */
5
+ export const snmpOf = (text: string, protocol: string): Record<string, number> => {
6
+ const lines = text.split("\n").filter((line) => line.startsWith(`${protocol}:`));
7
+ if (lines.length < 2) return {};
8
+ const names = lines[0]!.trim().split(/\s+/).slice(1);
9
+ const values = lines[1]!.trim().split(/\s+/).slice(1).map(Number);
10
+ return Object.fromEntries(names.map((name, at) => [name, values[at] ?? 0]));
11
+ };
12
+
13
+ /** These are the dropped packets of one interface: received and sent. */
14
+ export interface InterfaceDrops {
15
+ readonly rx: number;
16
+ readonly tx: number;
17
+ }
18
+
19
+ /** This function gives the dropped packets of each interface of `/proc/net/dev`. The fourth field of each direction is its drops. */
20
+ export const devOf = (text: string): Record<string, InterfaceDrops> => {
21
+ const out: Record<string, InterfaceDrops> = {};
22
+ for (const line of text.split("\n").slice(2)) {
23
+ const match = /^\s*([^:\s]+):\s*(.*)$/.exec(line);
24
+ if (match === null) continue;
25
+ const fields = match[2]!.trim().split(/\s+/).map(Number);
26
+ out[match[1]!] = { rx: fields[3] ?? 0, tx: fields[11] ?? 0 };
27
+ }
28
+ return out;
29
+ };
30
+
31
+ /**
32
+ * This function gives two sums over the CPUs of `/proc/net/softnet_stat`. The first is the packets
33
+ * that a full backlog dropped. The second is the times that a softirq used all of its budget. Each
34
+ * line is one CPU in hexadecimal: the processed packets, the drops, then the squeezes.
35
+ */
36
+ export const softnetOf = (text: string): { readonly dropped: number; readonly squeezed: number } => {
37
+ let dropped = 0;
38
+ let squeezed = 0;
39
+ for (const line of text.trim().split("\n")) {
40
+ const fields = line.trim().split(/\s+/);
41
+ if (fields.length < 3) continue;
42
+ dropped += parseInt(fields[1]!, 16);
43
+ squeezed += parseInt(fields[2]!, 16);
44
+ }
45
+ return { dropped, squeezed };
46
+ };
47
+
48
+ /** This function counts the entries of `/proc/net/arp` with no address yet (flags 0x0). A packet to such a neighbor waits in a queue, then drops. */
49
+ export const unresolvedOf = (text: string): number =>
50
+ text
51
+ .split("\n")
52
+ .slice(1)
53
+ .filter((line) => line.trim() !== "" && line.trim().split(/\s+/)[2] === "0x0").length;
54
+
55
+ /** This function gives each counter that grew from one read to the next, with its growth. A counter of the second read only is not in it. */
56
+ export const grownOf = (before: Readonly<Record<string, number>>, after: Readonly<Record<string, number>>): Record<string, number> => {
57
+ const out: Record<string, number> = {};
58
+ for (const [name, value] of Object.entries(after)) {
59
+ const last = before[name];
60
+ if (last !== undefined && value > last) out[name] = value - last;
61
+ }
62
+ return out;
63
+ };