@mark1russell7/netdiag 0.1.0 → 0.1.1

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.
Files changed (45) hide show
  1. package/README.md +153 -1
  2. package/build/address.d.ts +56 -0
  3. package/build/address.d.ts.map +1 -0
  4. package/build/address.js +172 -0
  5. package/build/address.js.map +1 -0
  6. package/build/{diagnosis-BTa-Dfwl.js → diagnosis-a4hRzFxp.js} +113 -11
  7. package/build/diagnosis-a4hRzFxp.js.map +1 -0
  8. package/build/diagnosis.d.ts +3 -2
  9. package/build/diagnosis.js +1 -1
  10. package/build/{index-CP1hzELI.d.ts → index-K2dGiYxW.d.ts} +83 -22
  11. package/build/index-K2dGiYxW.d.ts.map +1 -0
  12. package/build/index.d.ts +253 -148
  13. package/build/index.d.ts.map +1 -1
  14. package/build/index.js +1084 -161
  15. package/build/index.js.map +1 -1
  16. package/build/kinds-CQv769lK.js +341 -0
  17. package/build/kinds-CQv769lK.js.map +1 -0
  18. package/build/kinds-_NShfZpU.d.ts +377 -0
  19. package/build/kinds-_NShfZpU.d.ts.map +1 -0
  20. package/build/layer-BNg0VYuK.d.ts +22 -0
  21. package/build/layer-BNg0VYuK.d.ts.map +1 -0
  22. package/build/tools.d.ts +206 -0
  23. package/build/tools.d.ts.map +1 -0
  24. package/build/tools.js +896 -0
  25. package/build/tools.js.map +1 -0
  26. package/package.json +17 -5
  27. package/src/address/index.ts +155 -0
  28. package/src/diagnosis/catalog.ts +8 -8
  29. package/src/diagnosis/index.ts +13 -0
  30. package/src/diagnosis/score.ts +160 -0
  31. package/src/index.ts +35 -0
  32. package/src/kinds.ts +460 -0
  33. package/src/probes/native.ts +354 -0
  34. package/src/probes/socket.ts +520 -0
  35. package/src/readers.ts +367 -0
  36. package/src/scope.ts +5 -24
  37. package/src/tools/common.ts +47 -0
  38. package/src/tools/host.ts +91 -0
  39. package/src/tools/index.ts +38 -0
  40. package/src/tools/licenses.ts +37 -0
  41. package/src/tools/mtr.ts +156 -0
  42. package/src/tools/ping.ts +231 -0
  43. package/src/tools/traceroute.ts +313 -0
  44. package/build/diagnosis-BTa-Dfwl.js.map +0 -1
  45. package/build/index-CP1hzELI.d.ts.map +0 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This package has the core model of netdiag. It has the typed facts at each layer of the stack, the contract of a probe and the scope rule. It also has the catalog of faults and the probability model.
4
4
 
5
- **State: ND-1 and the model of ND-4 of the [roadmap](../../docs/ROADMAP.md).** The native probes (ND-2) come next. Then the readers change their facts into evidence for the model.
5
+ **State: ND-1, ND-2, ND-4, and the lab score and the loopback lab of ND-7 of the [roadmap](../../docs/ROADMAP.md).** The kernel lab and the bars of the gate come next.
6
6
 
7
7
  ## Facts
8
8
 
@@ -43,6 +43,95 @@ An active probe sends packets only to a peer that the user owns or names. The ru
43
43
 
44
44
  The scope also limits the number of ports of each host. The default limit is 8. Thus netdiag does no port scan.
45
45
 
46
+ ## IP addresses
47
+
48
+ The entry `@mark1russell7/netdiag/address` has plain functions for IP addresses and networks. It uses only the module `node:net`.
49
+
50
+ | Function | What it gives |
51
+ | --- | --- |
52
+ | `addressFamily(address)` | `ipv4`, `ipv6`, or `undefined` for a text that is not an address. |
53
+ | `addressClass(address)` | The class of the address, from the special-purpose ranges of RFC 6890. |
54
+ | `addressKind(address)` | `private` for an address that the internet does not route to, else `public`. |
55
+ | `parseCidr(text)` | A CIDR range, or the problem of the text. |
56
+ | `cidrSet(ranges)` | A set of ranges. Its `has(address)` tells if an address is in a range of the set. |
57
+
58
+ - The classes are `unspecified`, `loopback`, `private`, `shared`, `link-local`, `unique-local`, `documentation`, `benchmarking`, `multicast`, `reserved` and `public`.
59
+ - The table `ADDRESS_CLASSES` holds the ranges of the classes as data.
60
+ - An IPv4 address in IPv6 form, for example `::ffff:192.0.2.1`, is its IPv4 address.
61
+ - An address for documentation is `public`. Thus a lab can use `203.0.113.0/24` for hosts on the internet.
62
+
63
+ ```ts
64
+ import { addressClass, cidrSet, parseCidr } from "@mark1russell7/netdiag/address";
65
+
66
+ addressClass("100.64.0.1"); // "shared": the carrier-grade NAT range
67
+ const lab = parseCidr("203.0.113.0/24");
68
+ if (lab.tag === "ok") cidrSet([lab.value]).has("::ffff:203.0.113.7"); // true
69
+ ```
70
+
71
+ ## Native probes
72
+
73
+ The native probes use only the `node:` modules. They need no outside tool.
74
+
75
+ | Probe | Id | Facts |
76
+ | --- | --- | --- |
77
+ | `DNS_PROBE` | `dns.query` | `dns.answer`: the outcome, the addresses and the lowest TTL of one query for `A` or `AAAA` records. |
78
+ | `tcpProbe()` | `tcp.connect` | `tcp.connect`: the outcome and the time of one connect. |
79
+ | `tlsProbe()` | `tls.handshake` | `tcp.connect` and `tls.handshake`: the time of the handshake, the version, the cipher, ALPN and the certificate. |
80
+ | `httpProbe()` | `http.request` | `tcp.connect`, `tls.handshake` for `https`, and `http.response`: the status, some fields of the head and the time to the first byte. |
81
+
82
+ - A probe connects only to an IP address. The DNS probe gives the addresses of a name first. Thus the runner examines each address against the scope.
83
+ - The peer of the DNS probe is the resolver, not the name.
84
+ - A time limit or a stop of the signal closes the socket. The probe then gives the outcome `timeout`, or the runner gives `aborted`.
85
+ - The TLS probe does not refuse a certificate that is not valid. It records the problem, for example `expired`, `name` or `untrusted`.
86
+ - The trust tells the root of the chain. `public` is a root of Node.js. `private` is a different root, for example the root of a proxy that examines TLS.
87
+ - The option `roots` adds roots that the user trusts, for example the root of a company. Their trust is `private`.
88
+ - The option `publicRoots` adds roots with the trust `public`. A lab gives its own root here, so its servers stand for servers on the internet.
89
+ - The HTTP probe follows no redirect. It reads the body up to 64 KiB.
90
+ - For `https`, the HTTP probe sends its request only when the certificate passes its check. Thus an interceptor does not get the path.
91
+
92
+ ```ts
93
+ import { realClock } from "@mark1russell7/algebra/clock";
94
+ import { TCP_PROBE, createRunner, scopeOf } from "@mark1russell7/netdiag";
95
+
96
+ const scope = scopeOf([{ kind: "range", cidr: "192.168.1.0/24", basis: "owned" }]);
97
+ if (scope.tag === "err") throw new Error(scope.error.join(" "));
98
+
99
+ const runner = createRunner({ scope: scope.value, clock: realClock });
100
+ const outcome = await runner.run(TCP_PROBE, { address: "192.168.1.1", port: 443 });
101
+ // The fact tcp.connect has the outcome connected, refused, timeout, unreachable, reset or error.
102
+ ```
103
+
104
+ ## Outside tools
105
+
106
+ The entry `@mark1russell7/netdiag/tools` has optional adapters of outside tools. netdiag bundles no tool. The user installs each tool.
107
+
108
+ | Probe | Id | Tool | Facts |
109
+ | --- | --- | --- | --- |
110
+ | `pingProbe(host)` | `icmp.ping` | `ping` | `icmp.echo`: the echo replies, their round-trip times, and the messages of an unreachable address. |
111
+ | `tracerouteProbe(host)` | `path.traceroute` | `traceroute`, `tracert` on Windows | `path.trace`: the routers that answered each TTL, their round-trip times, and the messages of an unreachable address. |
112
+ | `mtrProbe(host)` | `path.mtr` | `mtr` (Linux and macOS) | `path.report`: the loss and the round-trip times of each hop over many cycles. |
113
+
114
+ - A host starts each tool. Its `run(command, args, options)` starts a program with a list of arguments and no shell.
115
+ - The function `runCommand` of `@procedures/shell` is a host with no change. A test gives a fake host.
116
+ - An adapter reads only English output. On Linux and macOS, it sets `LC_ALL=C`. On Windows, the language of the system sets the output.
117
+ - On Linux, the adapter reads the `ping` of iputils. On macOS, it uses `ping6` for an IPv6 address.
118
+ - On Linux, the trace adapter reads the Modern traceroute (the package `traceroute`). On macOS, it uses `traceroute6` for an IPv6 address.
119
+ - The `mtr` adapter reads the JSON report of `mtr --json` (version 0.92 or later). The default of 20 cycles takes approximately 20 seconds.
120
+ - Output that netdiag cannot read gives the outcome `error` with the reason. It does not give a reply that did not occur.
121
+ - The table `TOOL_LICENSES` holds the license of each tool as data. A tool with a restrictive license has no adapter.
122
+
123
+ ```ts
124
+ import { runCommand } from "@procedures/shell";
125
+ import { pingProbe, type ToolHost } from "@mark1russell7/netdiag/tools";
126
+
127
+ const host: ToolHost = {
128
+ platform: process.platform,
129
+ run: (command, args, options) => runCommand(command, { args: [...args], timeout: options.timeoutMs, signal: options.signal, env: options.env }),
130
+ };
131
+ // The runner comes from the example of the native probes.
132
+ const outcome = await runner.run(pingProbe(host), { address: "192.168.1.1", count: 20 });
133
+ ```
134
+
46
135
  ## Diagnosis
47
136
 
48
137
  The entry `@mark1russell7/netdiag/diagnosis` has the probability model. It imports no `node:` module, so a browser can also use it.
@@ -73,6 +162,69 @@ const result = diagnose(STANDARD_CATALOG, [
73
162
  // result.causes[0].id is "filter.icmp": a filter blocks ICMP, and the host is up.
74
163
  ```
75
164
 
165
+ ## Readers
166
+
167
+ `readEvidence(facts, options)` changes the facts of the native probes and of the adapters into evidence for the model. Each value that it gives is a value of a feature of `STANDARD_CATALOG`.
168
+
169
+ | Feature | The source of the value |
170
+ | --- | --- |
171
+ | `dns.answer` | The outcome of each DNS query. One answer `ok` for a name and a resolver decides. `nodata` gives `ok`, because the name exists. |
172
+ | `dns.address` | The addresses of a public name. One private address decides. A local name, for example `nas.local`, gives no value. |
173
+ | `ip.family` | The TCP connects to the IPv4 and the IPv6 addresses of one name. |
174
+ | `tcp.connect`, `tcp.time` | The outcome and the time of each TCP connect. A time of 900 ms or more tells that the SYN went again. |
175
+ | `tls.handshake`, `tls.trust` | The outcome of the handshake and the root of the certificate chain. A chain with no root of the store gives `private`. |
176
+ | `tls.ratio` | The time of the handshake divided by the time of the last TCP connect to the same address and port. |
177
+ | `http.status`, `http.ttfb` | The class of the status, and the time to the first byte divided by the time of the TCP connect. |
178
+ | `gateway.reply` | The echo requests to the address of the option `gateway`. An unreachable gateway gives `none`. |
179
+ | `icmp.reply` | The echo requests to each other address: `reply`, `none` or `unreachable`. |
180
+ | `path.loss` | The share of the echo requests with a reply, for 20 requests or more: `none`, `some` (up to 5 % loss) or `high`. A report of `mtr` that reached the address gives it from its last hop. |
181
+
182
+ - The reader gives one evidence for each feature, subject and role. When a subject has more facts, the last fact decides.
183
+ - A fact that gives no evidence goes in `skipped`, with the reason. For example, the catalog has no value for the TLS outcome `not-tls` yet.
184
+ - A fact `path.trace` gives no evidence yet. The catalog has no feature for the hops of a path. The lab runs (ND-7) fit one.
185
+ - The option `primary` tells which facts are about the target. The other facts are about a reference host.
186
+ - The option `gateway` gives the IP address of the default gateway.
187
+
188
+ ```ts
189
+ import { STANDARD_CATALOG, TCP_PROBE, diagnose, readEvidence } from "@mark1russell7/netdiag";
190
+
191
+ // The runner comes from the example of the native probes.
192
+ const outcome = await runner.run(TCP_PROBE, { address: "192.168.1.1", port: 443 });
193
+ if (outcome.status === "done") {
194
+ const { evidence, skipped } = readEvidence(outcome.facts);
195
+ const result = diagnose(STANDARD_CATALOG, evidence);
196
+ // A connect with the outcome refused gives result.causes[0].id "port.closed".
197
+ }
198
+ ```
199
+
200
+ ## The lab score
201
+
202
+ A lab makes one fault on purpose. Then netdiag must name it. The entry `@mark1russell7/netdiag/diagnosis` has the score of these runs.
203
+
204
+ | Function | What it gives |
205
+ | --- | --- |
206
+ | `labRecordOf(catalog, case, truth, diagnosis)` | The record of one run: the true hypothesis, the predicted cause, the rank of the truth and the probability of each cause. |
207
+ | `scoreLab(catalog, records)` | The confusion table, the recall of each hypothesis with its Wilson interval, the accuracy and the mean Brier score. |
208
+ | `labGate(score, bars)` | A verdict for each hypothesis with a bar: `pass`, `fail` or `no-read`. |
209
+
210
+ - The recall of a hypothesis is the part of its runs where the diagnosis names it.
211
+ - A hypothesis passes when the lower end of its recall interval is its bar or more.
212
+ - A hypothesis with no runs gets `no-read`, and the gate does not pass.
213
+ - A hypothesis with no bar goes in `unbarred`, for example a fault that no lab can make.
214
+
215
+ ```ts
216
+ import { STANDARD_CATALOG, diagnose, labGate, labRecordOf, scoreLab } from "@mark1russell7/netdiag/diagnosis";
217
+
218
+ const diagnosis = diagnose(STANDARD_CATALOG, [{ feature: "tcp.connect", value: "refused", primary: true }]);
219
+ const record = labRecordOf(STANDARD_CATALOG, "port.closed/refused", "port.closed", diagnosis);
220
+ if (record.tag === "err") throw new Error(record.error);
221
+
222
+ const score = scoreLab(STANDARD_CATALOG, [record.value]);
223
+ if (score.tag === "err") throw new Error(score.error.join(" "));
224
+ const gate = labGate(score.value, { "port.closed": 0.1 });
225
+ // gate.value.verdicts[0].status is "pass": the lower end of the recall of 1 run of 1 is 0.21.
226
+ ```
227
+
76
228
  ## Example
77
229
 
78
230
  ```ts
@@ -0,0 +1,56 @@
1
+ import { Result } from "@mark1russell7/optional";
2
+ /** The family of an IP address. */
3
+ export type AddressFamily = "ipv4" | "ipv6";
4
+ /**
5
+ * The class of an IP address, from the special-purpose ranges of RFC 6890.
6
+ *
7
+ * - `unspecified`: `0.0.0.0/8` and `::`.
8
+ * - `loopback`: `127.0.0.0/8` and `::1`.
9
+ * - `private`: the ranges of RFC 1918.
10
+ * - `shared`: `100.64.0.0/10`, the carrier-grade NAT range of RFC 6598.
11
+ * - `link-local`: `169.254.0.0/16` and `fe80::/10`.
12
+ * - `unique-local`: `fc00::/7`, the private range of IPv6 (RFC 4193).
13
+ * - `documentation`: the ranges of RFC 5737 and RFC 3849 for examples.
14
+ * - `benchmarking`: `198.18.0.0/15` (RFC 2544).
15
+ * - `multicast`: `224.0.0.0/4` and `ff00::/8`.
16
+ * - `reserved`: `240.0.0.0/4`, with the broadcast address.
17
+ * - `public`: each other address.
18
+ */
19
+ export type AddressClass = "unspecified" | "loopback" | "private" | "shared" | "link-local" | "unique-local" | "documentation" | "benchmarking" | "multicast" | "reserved" | "public";
20
+ /** A CIDR range: an address of the network and the length of its prefix. */
21
+ export interface Cidr {
22
+ readonly address: string;
23
+ readonly prefix: number;
24
+ readonly family: AddressFamily;
25
+ }
26
+ /** The special-purpose ranges, as data. An address that is in no range is `public`. */
27
+ export declare const ADDRESS_CLASSES: readonly {
28
+ readonly cidr: string;
29
+ readonly class: Exclude<AddressClass, "public">;
30
+ }[];
31
+ /** This function gives the canonical form of an address: no spaces, no brackets, lower case, and IPv4 for a mapped IPv4 address. */
32
+ export declare function canonicalAddress(address: string): string;
33
+ /** This function gives the family of an IP address, or `undefined` for a text that is not an IP address. */
34
+ export declare function addressFamily(address: string): AddressFamily | undefined;
35
+ /** This function gives the CIDR range of a text, for example `192.0.2.0/24` or `2001:db8::/32`. An address alone is a range of one address. */
36
+ export declare function parseCidr(text: string): Result<Cidr, string>;
37
+ /** This function gives the text of a CIDR range. */
38
+ export declare function formatCidr(cidr: Cidr): string;
39
+ /** A set of CIDR ranges. */
40
+ export interface CidrSet {
41
+ readonly ranges: readonly Cidr[];
42
+ /** This function tells if an address is in a range of the set. A text that is not an IP address is in no set. */
43
+ has(address: string): boolean;
44
+ }
45
+ /** This function gives the set of some CIDR ranges. */
46
+ export declare function cidrSet(ranges: readonly Cidr[]): CidrSet;
47
+ /** This function tells if an address is in a CIDR range. */
48
+ export declare function cidrContains(cidr: Cidr, address: string): boolean;
49
+ /** This function gives the class of an IP address, or `undefined` for a text that is not an IP address. */
50
+ export declare function addressClass(address: string): AddressClass | undefined;
51
+ /**
52
+ * This function tells if the internet routes to an address. A `private` address is unspecified, loopback, private,
53
+ * shared, link-local or unique-local. Each other address is `public`, also an address for documentation.
54
+ */
55
+ export declare function addressKind(address: string): "public" | "private" | undefined;
56
+ //# sourceMappingURL=address.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.d.ts","names":[],"sources":["../src/address/index.ts"],"mappings":";;YASY;;;;;;;;;;;;;;;;YAiBA;;iBAcK;WACN;WACA;WACA,QAAQ;;;qBAIN;WAAqC;WAAuB,OAAO,QAAQ;;;wBA0BxE,iBAAiB;;wBAQjB,cAAc,kBAAkB;;wBAMhC,UAAU,eAAe,OAAO;;wBAYhC,WAAW,MAAM;;iBAKhB;WACN,iBAAiB;;EAE1B,IAAI;;;wBAIU,QAAQ,iBAAiB,SAAS;;wBAclC,aAAa,MAAM,MAAM;;wBAgBzB,aAAa,kBAAkB;;;;;wBAS/B,YAAY"}
@@ -0,0 +1,172 @@
1
+ import { BlockList, isIP } from "node:net";
2
+ import { err, ok } from "@mark1russell7/optional";
3
+ // src/address/index.ts
4
+ /**
5
+ * The IP addresses and the networks of netdiag: the family and the class of an address, CIDR ranges and sets of ranges.
6
+ *
7
+ * This entry uses only the module `node:net`. An IPv4 address in IPv6 form (`::ffff:192.0.2.1`) is its IPv4 address.
8
+ */
9
+ /** The special-purpose ranges, as data. An address that is in no range is `public`. */
10
+ const ADDRESS_CLASSES = [
11
+ {
12
+ cidr: "0.0.0.0/8",
13
+ class: "unspecified"
14
+ },
15
+ {
16
+ cidr: "10.0.0.0/8",
17
+ class: "private"
18
+ },
19
+ {
20
+ cidr: "100.64.0.0/10",
21
+ class: "shared"
22
+ },
23
+ {
24
+ cidr: "127.0.0.0/8",
25
+ class: "loopback"
26
+ },
27
+ {
28
+ cidr: "169.254.0.0/16",
29
+ class: "link-local"
30
+ },
31
+ {
32
+ cidr: "172.16.0.0/12",
33
+ class: "private"
34
+ },
35
+ {
36
+ cidr: "192.0.2.0/24",
37
+ class: "documentation"
38
+ },
39
+ {
40
+ cidr: "192.168.0.0/16",
41
+ class: "private"
42
+ },
43
+ {
44
+ cidr: "198.18.0.0/15",
45
+ class: "benchmarking"
46
+ },
47
+ {
48
+ cidr: "198.51.100.0/24",
49
+ class: "documentation"
50
+ },
51
+ {
52
+ cidr: "203.0.113.0/24",
53
+ class: "documentation"
54
+ },
55
+ {
56
+ cidr: "224.0.0.0/4",
57
+ class: "multicast"
58
+ },
59
+ {
60
+ cidr: "240.0.0.0/4",
61
+ class: "reserved"
62
+ },
63
+ {
64
+ cidr: "::/128",
65
+ class: "unspecified"
66
+ },
67
+ {
68
+ cidr: "::1/128",
69
+ class: "loopback"
70
+ },
71
+ {
72
+ cidr: "2001:db8::/32",
73
+ class: "documentation"
74
+ },
75
+ {
76
+ cidr: "fc00::/7",
77
+ class: "unique-local"
78
+ },
79
+ {
80
+ cidr: "fe80::/10",
81
+ class: "link-local"
82
+ },
83
+ {
84
+ cidr: "ff00::/8",
85
+ class: "multicast"
86
+ }
87
+ ];
88
+ /** The classes of the addresses that the internet does not route to a host. */
89
+ const NOT_ROUTED = /* @__PURE__ */ new Set([
90
+ "unspecified",
91
+ "loopback",
92
+ "private",
93
+ "shared",
94
+ "link-local",
95
+ "unique-local"
96
+ ]);
97
+ /** This function gives the canonical form of an address: no spaces, no brackets, lower case, and IPv4 for a mapped IPv4 address. */
98
+ function canonicalAddress(address) {
99
+ let text = address.trim().toLowerCase();
100
+ if (text.startsWith("[") && text.endsWith("]")) text = text.slice(1, -1);
101
+ return /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(text)?.[1] ?? text;
102
+ }
103
+ /** This function gives the family of an IP address, or `undefined` for a text that is not an IP address. */
104
+ function addressFamily(address) {
105
+ const family = isIP(canonicalAddress(address));
106
+ return family === 4 ? "ipv4" : family === 6 ? "ipv6" : void 0;
107
+ }
108
+ /** This function gives the CIDR range of a text, for example `192.0.2.0/24` or `2001:db8::/32`. An address alone is a range of one address. */
109
+ function parseCidr(text) {
110
+ const slash = text.indexOf("/");
111
+ const address = canonicalAddress(slash === -1 ? text : text.slice(0, slash));
112
+ const family = addressFamily(address);
113
+ if (family === void 0) return err(`"${text}" is not an IP address or a CIDR range.`);
114
+ const max = family === "ipv4" ? 32 : 128;
115
+ const prefix = slash === -1 ? String(max) : text.slice(slash + 1).trim();
116
+ if (!/^\d{1,3}$/.test(prefix) || Number(prefix) > max) return err(`"${text}" has no prefix from 0 to ${max}.`);
117
+ return ok({
118
+ address,
119
+ prefix: Number(prefix),
120
+ family
121
+ });
122
+ }
123
+ /** This function gives the text of a CIDR range. */
124
+ function formatCidr(cidr) {
125
+ return `${cidr.address}/${cidr.prefix}`;
126
+ }
127
+ /** This function gives the set of some CIDR ranges. */
128
+ function cidrSet(ranges) {
129
+ const list = new BlockList();
130
+ for (const range of ranges) list.addSubnet(range.address, range.prefix, range.family);
131
+ return {
132
+ ranges: [...ranges],
133
+ has(address) {
134
+ const host = canonicalAddress(address);
135
+ const family = addressFamily(host);
136
+ return family !== void 0 && list.check(host, family);
137
+ }
138
+ };
139
+ }
140
+ /** This function tells if an address is in a CIDR range. */
141
+ function cidrContains(cidr, address) {
142
+ return cidrSet([cidr]).has(address);
143
+ }
144
+ let classSets;
145
+ function classes() {
146
+ classSets ??= ADDRESS_CLASSES.map((entry) => {
147
+ const range = parseCidr(entry.cidr);
148
+ if (range.tag === "err") throw new Error(range.error);
149
+ return {
150
+ class: entry.class,
151
+ set: cidrSet([range.value])
152
+ };
153
+ });
154
+ return classSets;
155
+ }
156
+ /** This function gives the class of an IP address, or `undefined` for a text that is not an IP address. */
157
+ function addressClass(address) {
158
+ if (addressFamily(address) === void 0) return void 0;
159
+ return classes().find((entry) => entry.set.has(address))?.class ?? "public";
160
+ }
161
+ /**
162
+ * This function tells if the internet routes to an address. A `private` address is unspecified, loopback, private,
163
+ * shared, link-local or unique-local. Each other address is `public`, also an address for documentation.
164
+ */
165
+ function addressKind(address) {
166
+ const found = addressClass(address);
167
+ if (found === void 0) return void 0;
168
+ return NOT_ROUTED.has(found) ? "private" : "public";
169
+ }
170
+ export { ADDRESS_CLASSES, addressClass, addressFamily, addressKind, canonicalAddress, cidrContains, cidrSet, formatCidr, parseCidr };
171
+
172
+ //# sourceMappingURL=address.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.js","names":[],"sources":["../src/address/index.ts"],"sourcesContent":["/**\n * The IP addresses and the networks of netdiag: the family and the class of an address, CIDR ranges and sets of ranges.\n *\n * This entry uses only the module `node:net`. An IPv4 address in IPv6 form (`::ffff:192.0.2.1`) is its IPv4 address.\n */\nimport { BlockList, isIP } from \"node:net\";\nimport { err, ok, type Result } from \"@mark1russell7/optional\";\n\n/** The family of an IP address. */\nexport type AddressFamily = \"ipv4\" | \"ipv6\";\n\n/**\n * The class of an IP address, from the special-purpose ranges of RFC 6890.\n *\n * - `unspecified`: `0.0.0.0/8` and `::`.\n * - `loopback`: `127.0.0.0/8` and `::1`.\n * - `private`: the ranges of RFC 1918.\n * - `shared`: `100.64.0.0/10`, the carrier-grade NAT range of RFC 6598.\n * - `link-local`: `169.254.0.0/16` and `fe80::/10`.\n * - `unique-local`: `fc00::/7`, the private range of IPv6 (RFC 4193).\n * - `documentation`: the ranges of RFC 5737 and RFC 3849 for examples.\n * - `benchmarking`: `198.18.0.0/15` (RFC 2544).\n * - `multicast`: `224.0.0.0/4` and `ff00::/8`.\n * - `reserved`: `240.0.0.0/4`, with the broadcast address.\n * - `public`: each other address.\n */\nexport type AddressClass =\n | \"unspecified\"\n | \"loopback\"\n | \"private\"\n | \"shared\"\n | \"link-local\"\n | \"unique-local\"\n | \"documentation\"\n | \"benchmarking\"\n | \"multicast\"\n | \"reserved\"\n | \"public\";\n\n/** A CIDR range: an address of the network and the length of its prefix. */\nexport interface Cidr {\n readonly address: string;\n readonly prefix: number;\n readonly family: AddressFamily;\n}\n\n/** The special-purpose ranges, as data. An address that is in no range is `public`. */\nexport const ADDRESS_CLASSES: readonly { readonly cidr: string; readonly class: Exclude<AddressClass, \"public\"> }[] = [\n { cidr: \"0.0.0.0/8\", class: \"unspecified\" },\n { cidr: \"10.0.0.0/8\", class: \"private\" },\n { cidr: \"100.64.0.0/10\", class: \"shared\" },\n { cidr: \"127.0.0.0/8\", class: \"loopback\" },\n { cidr: \"169.254.0.0/16\", class: \"link-local\" },\n { cidr: \"172.16.0.0/12\", class: \"private\" },\n { cidr: \"192.0.2.0/24\", class: \"documentation\" },\n { cidr: \"192.168.0.0/16\", class: \"private\" },\n { cidr: \"198.18.0.0/15\", class: \"benchmarking\" },\n { cidr: \"198.51.100.0/24\", class: \"documentation\" },\n { cidr: \"203.0.113.0/24\", class: \"documentation\" },\n { cidr: \"224.0.0.0/4\", class: \"multicast\" },\n { cidr: \"240.0.0.0/4\", class: \"reserved\" },\n { cidr: \"::/128\", class: \"unspecified\" },\n { cidr: \"::1/128\", class: \"loopback\" },\n { cidr: \"2001:db8::/32\", class: \"documentation\" },\n { cidr: \"fc00::/7\", class: \"unique-local\" },\n { cidr: \"fe80::/10\", class: \"link-local\" },\n { cidr: \"ff00::/8\", class: \"multicast\" },\n];\n\n/** The classes of the addresses that the internet does not route to a host. */\nconst NOT_ROUTED: ReadonlySet<AddressClass> = new Set([\"unspecified\", \"loopback\", \"private\", \"shared\", \"link-local\", \"unique-local\"]);\n\n/** This function gives the canonical form of an address: no spaces, no brackets, lower case, and IPv4 for a mapped IPv4 address. */\nexport function canonicalAddress(address: string): string {\n let text = address.trim().toLowerCase();\n if (text.startsWith(\"[\") && text.endsWith(\"]\")) text = text.slice(1, -1);\n const mapped = /^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(text)?.[1];\n return mapped ?? text;\n}\n\n/** This function gives the family of an IP address, or `undefined` for a text that is not an IP address. */\nexport function addressFamily(address: string): AddressFamily | undefined {\n const family = isIP(canonicalAddress(address));\n return family === 4 ? \"ipv4\" : family === 6 ? \"ipv6\" : undefined;\n}\n\n/** This function gives the CIDR range of a text, for example `192.0.2.0/24` or `2001:db8::/32`. An address alone is a range of one address. */\nexport function parseCidr(text: string): Result<Cidr, string> {\n const slash = text.indexOf(\"/\");\n const address = canonicalAddress(slash === -1 ? text : text.slice(0, slash));\n const family = addressFamily(address);\n if (family === undefined) return err(`\"${text}\" is not an IP address or a CIDR range.`);\n const max = family === \"ipv4\" ? 32 : 128;\n const prefix = slash === -1 ? String(max) : text.slice(slash + 1).trim();\n if (!/^\\d{1,3}$/.test(prefix) || Number(prefix) > max) return err(`\"${text}\" has no prefix from 0 to ${max}.`);\n return ok({ address, prefix: Number(prefix), family });\n}\n\n/** This function gives the text of a CIDR range. */\nexport function formatCidr(cidr: Cidr): string {\n return `${cidr.address}/${cidr.prefix}`;\n}\n\n/** A set of CIDR ranges. */\nexport interface CidrSet {\n readonly ranges: readonly Cidr[];\n /** This function tells if an address is in a range of the set. A text that is not an IP address is in no set. */\n has(address: string): boolean;\n}\n\n/** This function gives the set of some CIDR ranges. */\nexport function cidrSet(ranges: readonly Cidr[]): CidrSet {\n const list = new BlockList();\n for (const range of ranges) list.addSubnet(range.address, range.prefix, range.family);\n return {\n ranges: [...ranges],\n has(address) {\n const host = canonicalAddress(address);\n const family = addressFamily(host);\n return family !== undefined && list.check(host, family);\n },\n };\n}\n\n/** This function tells if an address is in a CIDR range. */\nexport function cidrContains(cidr: Cidr, address: string): boolean {\n return cidrSet([cidr]).has(address);\n}\n\nlet classSets: readonly { readonly class: Exclude<AddressClass, \"public\">; readonly set: CidrSet }[] | undefined;\n\nfunction classes(): NonNullable<typeof classSets> {\n classSets ??= ADDRESS_CLASSES.map((entry) => {\n const range = parseCidr(entry.cidr);\n if (range.tag === \"err\") throw new Error(range.error);\n return { class: entry.class, set: cidrSet([range.value]) };\n });\n return classSets;\n}\n\n/** This function gives the class of an IP address, or `undefined` for a text that is not an IP address. */\nexport function addressClass(address: string): AddressClass | undefined {\n if (addressFamily(address) === undefined) return undefined;\n return classes().find((entry) => entry.set.has(address))?.class ?? \"public\";\n}\n\n/**\n * This function tells if the internet routes to an address. A `private` address is unspecified, loopback, private,\n * shared, link-local or unique-local. Each other address is `public`, also an address for documentation.\n */\nexport function addressKind(address: string): \"public\" | \"private\" | undefined {\n const found = addressClass(address);\n if (found === undefined) return undefined;\n return NOT_ROUTED.has(found) ? \"private\" : \"public\";\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { err, ok } from "@mark1russell7/optional";
2
+ import { brier, classScores, confusion, wilson } from "@mark1russell7/rigor-statistics";
2
3
  // src/layer.ts
3
4
  /**
4
5
  * The layers of the network stack that netdiag examines, from the bottom to the top.
@@ -298,26 +299,28 @@ const STANDARD_FEATURES = [
298
299
  {
299
300
  id: "dns.answer",
300
301
  layer: "dns",
301
- summary: "The result of the DNS lookup of the name of the target.",
302
+ summary: "The result of the DNS lookup of the name of the target. The value unreachable tells that no DNS service listens on the resolver.",
302
303
  values: [
303
304
  "ok",
304
305
  "nxdomain",
305
306
  "servfail",
306
307
  "refused",
307
- "timeout"
308
+ "timeout",
309
+ "unreachable"
308
310
  ],
309
311
  healthy: {
310
- ok: .97,
312
+ ok: .965,
311
313
  nxdomain: .01,
312
314
  servfail: .005,
313
315
  refused: .005,
314
- timeout: .01
316
+ timeout: .01,
317
+ unreachable: .005
315
318
  }
316
319
  },
317
320
  {
318
321
  id: "dns.address",
319
322
  layer: "dns",
320
- summary: "The kind of the addresses of a public name: public, or private (RFC 1918, RFC 4193 or loopback).",
323
+ summary: "The kind of the addresses of a public name: public, or private (RFC 1918, RFC 6598, RFC 4193, loopback, link-local or unspecified).",
321
324
  values: ["public", "private"],
322
325
  healthy: {
323
326
  public: .95,
@@ -410,7 +413,7 @@ const STANDARD_FEATURES = [
410
413
  {
411
414
  id: "tls.ratio",
412
415
  layer: "tls",
413
- summary: "The time of the TLS handshake divided by the time of the TCP connect: low (below 3), mid, or high (above 8).",
416
+ summary: "The time of the TLS handshake divided by the time of the TCP connect: low (below 3), mid, or high (8 or more).",
414
417
  values: [
415
418
  "low",
416
419
  "mid",
@@ -452,7 +455,7 @@ const STANDARD_FEATURES = [
452
455
  {
453
456
  id: "http.ttfb",
454
457
  layer: "http",
455
- summary: "The time to the first byte divided by the time of the TCP connect: low (below 4), mid, or high (above 20).",
458
+ summary: "The time to the first byte divided by the time of the TCP connect: low (below 4), mid, or high (20 or more).",
456
459
  values: [
457
460
  "low",
458
461
  "mid",
@@ -573,7 +576,7 @@ const STANDARD_FAULTS = [
573
576
  summary: "The DNS resolver does not answer.",
574
577
  step: "Use a different DNS resolver, or examine the resolver of the router.",
575
578
  predicts: { "dns.answer": {
576
- expect: "timeout",
579
+ expect: ["timeout", "unreachable"],
577
580
  strength: "strong"
578
581
  } }
579
582
  },
@@ -805,7 +808,7 @@ const STANDARD_FAULTS = [
805
808
  ];
806
809
  /** The standard catalog: the rules of practice as the first likelihoods. Each hypothesis has the prior weight 1. */
807
810
  const STANDARD_CATALOG_SPEC = {
808
- version: "1",
811
+ version: "2",
809
812
  features: STANDARD_FEATURES,
810
813
  faults: STANDARD_FAULTS
811
814
  };
@@ -815,6 +818,105 @@ const STANDARD_CATALOG = (() => {
815
818
  if (compiled.tag === "err") throw new Error(`The standard catalog has problems: ${compiled.error.join(" ")}`);
816
819
  return compiled.value;
817
820
  })();
818
- export { layerRank as _, FLOOR as a, STRENGTHS as c, diagnose as d, expand as f, isLayer as g, LAYERS as h, STANDARD_FEATURES as i, UNKNOWN as l, predictive as m, STANDARD_CATALOG_SPEC as n, NONE as o, explain as p, STANDARD_FAULTS as r, PARTS as s, STANDARD_CATALOG as t, compileCatalog as u };
821
+ // src/diagnosis/score.ts
822
+ /** The format of a lab record. */
823
+ const LAB_RECORD_FORMAT = "netdiag.lab-record/1";
824
+ /**
825
+ * This function makes the record of one run of a lab case. The truth must be a hypothesis of the catalog, and the
826
+ * diagnosis must have a cause.
827
+ */
828
+ function labRecordOf(catalog, caseId, truth, diagnosis) {
829
+ if (!catalog.hypotheses.some((hypothesis) => hypothesis.id === truth)) return err(`The truth "${truth}" is not a hypothesis of the catalog ${catalog.version}.`);
830
+ const first = diagnosis.causes[0];
831
+ if (first === void 0) return err("The diagnosis has no cause.");
832
+ const index = diagnosis.causes.findIndex((cause) => cause.id === truth);
833
+ if (index < 0) return err(`The diagnosis has no cause "${truth}".`);
834
+ return ok({
835
+ format: LAB_RECORD_FORMAT,
836
+ case: caseId,
837
+ catalog: catalog.version,
838
+ truth,
839
+ predicted: first.id,
840
+ rank: index + 1,
841
+ causes: diagnosis.causes.map((cause) => ({
842
+ id: cause.id,
843
+ probability: cause.probability
844
+ }))
845
+ });
846
+ }
847
+ /**
848
+ * This function scores the lab records of one catalog. The recall of a hypothesis is the part of its runs where the
849
+ * diagnosis names it. A record of a different catalog version, or with a hypothesis that is not in the catalog, is an error.
850
+ */
851
+ function scoreLab(catalog, records) {
852
+ const ids = catalog.hypotheses.map((hypothesis) => hypothesis.id);
853
+ const known = new Set(ids);
854
+ const problems = [];
855
+ records.forEach((record, i) => {
856
+ const name = `Record ${i} (${record.case})`;
857
+ if (record.format !== "netdiag.lab-record/1") problems.push(`${name} has the format "${String(record.format)}", not "${LAB_RECORD_FORMAT}".`);
858
+ if (record.catalog !== catalog.version) problems.push(`${name} comes from the catalog ${record.catalog}, not ${catalog.version}.`);
859
+ for (const id of /* @__PURE__ */ new Set([
860
+ record.truth,
861
+ record.predicted,
862
+ ...record.causes.map((cause) => cause.id)
863
+ ])) if (!known.has(id)) problems.push(`${name} names "${id}", which is not a hypothesis of the catalog.`);
864
+ });
865
+ if (problems.length > 0) return err(problems);
866
+ const table = confusion(records.map((record) => record.truth), records.map((record) => record.predicted), ids);
867
+ const forecasts = records.flatMap((record) => {
868
+ const probability = new Map(record.causes.map((cause) => [cause.id, cause.probability]));
869
+ return ids.map((id) => ({
870
+ p: probability.get(id) ?? 0,
871
+ hit: id === record.truth
872
+ }));
873
+ });
874
+ return ok({
875
+ catalog: catalog.version,
876
+ n: table.n,
877
+ correct: table.correct,
878
+ accuracy: wilson(table.correct, table.n),
879
+ table,
880
+ classes: classScores(table),
881
+ brier: records.length === 0 ? void 0 : brier(forecasts) * ids.length
882
+ });
883
+ }
884
+ /**
885
+ * This function holds the recall of each hypothesis with a bar. A hypothesis passes when the lower end of its
886
+ * recall interval is the bar or more. A bar for a hypothesis that is not in the score is an error.
887
+ */
888
+ function labGate(score, bars) {
889
+ const problems = [];
890
+ const labels = new Set(score.table.labels);
891
+ for (const [hypothesis, bar] of Object.entries(bars)) {
892
+ if (!labels.has(hypothesis)) problems.push(`The bar of "${hypothesis}" is not for a hypothesis of the catalog ${score.catalog}.`);
893
+ if (!(bar >= 0 && bar <= 1)) problems.push(`The bar of "${hypothesis}" is ${bar}. A bar is from 0 to 1.`);
894
+ }
895
+ if (problems.length > 0) return err(problems);
896
+ const verdicts = [];
897
+ const unbarred = [];
898
+ for (const scores of score.classes) {
899
+ const bar = bars[scores.label];
900
+ if (bar === void 0) {
901
+ unbarred.push(scores.label);
902
+ continue;
903
+ }
904
+ const low = scores.support === 0 ? void 0 : scores.recallInterval.low;
905
+ const status = low === void 0 ? "no-read" : low >= bar ? "pass" : "fail";
906
+ verdicts.push({
907
+ hypothesis: scores.label,
908
+ status,
909
+ bar,
910
+ low,
911
+ support: scores.support
912
+ });
913
+ }
914
+ return ok({
915
+ pass: verdicts.length > 0 && verdicts.every((verdict) => verdict.status === "pass"),
916
+ verdicts,
917
+ unbarred
918
+ });
919
+ }
920
+ export { explain as _, STANDARD_CATALOG as a, isLayer as b, STANDARD_FEATURES as c, PARTS as d, STRENGTHS as f, expand as g, diagnose as h, scoreLab as i, FLOOR as l, compileCatalog as m, labGate as n, STANDARD_CATALOG_SPEC as o, UNKNOWN as p, labRecordOf as r, STANDARD_FAULTS as s, LAB_RECORD_FORMAT as t, NONE as u, predictive as v, layerRank as x, LAYERS as y };
819
921
 
820
- //# sourceMappingURL=diagnosis-BTa-Dfwl.js.map
922
+ //# sourceMappingURL=diagnosis-a4hRzFxp.js.map