@mark1russell7/netdiag 0.0.0-bootstrap.0 → 0.1.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.
@@ -0,0 +1,357 @@
1
+ import { compileCatalog, type Catalog, type CatalogSpec, type Feature, type Fault } from "./model.ts";
2
+
3
+ /**
4
+ * The features of the standard catalog.
5
+ *
6
+ * The readers of the facts give these features. The distribution `healthy` of each feature is a rule of practice. The
7
+ * lab runs (ND-7) then fit it.
8
+ */
9
+ export const STANDARD_FEATURES: readonly Feature[] = [
10
+ {
11
+ id: "link.state",
12
+ layer: "link",
13
+ summary: "The state of the link of the network interface.",
14
+ values: ["up", "down"],
15
+ healthy: { up: 0.999, down: 0.001 },
16
+ },
17
+ {
18
+ id: "wifi.signal",
19
+ layer: "wifi",
20
+ summary: "The strength of the Wi-Fi signal: strong above -60 dBm, weak below -75 dBm.",
21
+ values: ["strong", "fair", "weak"],
22
+ healthy: { strong: 0.7, fair: 0.25, weak: 0.05 },
23
+ },
24
+ {
25
+ id: "gateway.reply",
26
+ layer: "route",
27
+ summary: "The gateway answers an ICMP echo request.",
28
+ values: ["reply", "none"],
29
+ healthy: { reply: 0.97, none: 0.03 },
30
+ },
31
+ {
32
+ id: "path.loss",
33
+ layer: "path",
34
+ summary: "The packet loss on the path: none, some (up to 5 %) or high (more than 5 %).",
35
+ values: ["none", "some", "high"],
36
+ healthy: { none: 0.9, some: 0.08, high: 0.02 },
37
+ },
38
+ {
39
+ id: "dns.answer",
40
+ layer: "dns",
41
+ summary: "The result of the DNS lookup of the name of the target.",
42
+ values: ["ok", "nxdomain", "servfail", "refused", "timeout"],
43
+ healthy: { ok: 0.97, nxdomain: 0.01, servfail: 0.005, refused: 0.005, timeout: 0.01 },
44
+ },
45
+ {
46
+ id: "dns.address",
47
+ layer: "dns",
48
+ summary: "The kind of the addresses of a public name: public, or private (RFC 1918, RFC 4193 or loopback).",
49
+ values: ["public", "private"],
50
+ healthy: { public: 0.95, private: 0.05 },
51
+ },
52
+ {
53
+ id: "ip.family",
54
+ layer: "ip",
55
+ summary: "The address families that connect, for a host with IPv4 and IPv6 addresses.",
56
+ values: ["both", "ipv4-only", "ipv6-only"],
57
+ healthy: { both: 0.9, "ipv4-only": 0.08, "ipv6-only": 0.02 },
58
+ },
59
+ {
60
+ id: "icmp.reply",
61
+ layer: "ip",
62
+ summary: "The host answers an ICMP echo request, does not answer, or a router tells that it is unreachable.",
63
+ values: ["reply", "none", "unreachable"],
64
+ healthy: { reply: 0.85, none: 0.14, unreachable: 0.01 },
65
+ },
66
+ {
67
+ id: "tcp.connect",
68
+ layer: "tcp",
69
+ summary: "The outcome of a TCP connect to the port of the target.",
70
+ values: ["connected", "refused", "timeout", "unreachable", "reset"],
71
+ healthy: { connected: 0.97, refused: 0.01, timeout: 0.01, unreachable: 0.005, reset: 0.005 },
72
+ },
73
+ {
74
+ id: "tcp.time",
75
+ layer: "tcp",
76
+ summary: "The time of the TCP connect: short (below 150 ms), long, or retry (900 ms or more, a SYN sent again).",
77
+ values: ["short", "long", "retry"],
78
+ healthy: { short: 0.8, long: 0.18, retry: 0.02 },
79
+ },
80
+ {
81
+ id: "tls.handshake",
82
+ layer: "tls",
83
+ summary: "The outcome of the TLS handshake.",
84
+ values: ["ok", "cert-error", "alert", "timeout", "reset"],
85
+ healthy: { ok: 0.97, "cert-error": 0.01, alert: 0.01, timeout: 0.005, reset: 0.005 },
86
+ },
87
+ {
88
+ id: "tls.ratio",
89
+ layer: "tls",
90
+ summary: "The time of the TLS handshake divided by the time of the TCP connect: low (below 3), mid, or high (above 8).",
91
+ values: ["low", "mid", "high"],
92
+ healthy: { low: 0.75, mid: 0.2, high: 0.05 },
93
+ },
94
+ {
95
+ id: "tls.trust",
96
+ layer: "tls",
97
+ summary: "The issuer of the certificate: a public root of trust, or a private root.",
98
+ values: ["public", "private"],
99
+ healthy: { public: 0.97, private: 0.03 },
100
+ },
101
+ {
102
+ id: "http.status",
103
+ layer: "http",
104
+ summary: "The class of the HTTP status: ok (2xx and 3xx), client-error (4xx), server-error (5xx), or none.",
105
+ values: ["ok", "client-error", "server-error", "none"],
106
+ healthy: { ok: 0.95, "client-error": 0.03, "server-error": 0.01, none: 0.01 },
107
+ },
108
+ {
109
+ id: "http.ttfb",
110
+ layer: "http",
111
+ summary: "The time to the first byte divided by the time of the TCP connect: low (below 4), mid, or high (above 20).",
112
+ values: ["low", "mid", "high"],
113
+ healthy: { low: 0.6, mid: 0.3, high: 0.1 },
114
+ },
115
+ ];
116
+
117
+ /**
118
+ * The faults of the standard catalog. Each prediction is a rule of practice. The lab runs (ND-7) then fit the
119
+ * distributions, and the priors.
120
+ */
121
+ export const STANDARD_FAULTS: readonly Fault[] = [
122
+ {
123
+ id: "interface.down",
124
+ part: "interface",
125
+ reach: "shared",
126
+ summary: "The network interface has no link.",
127
+ step: "Connect the cable or the Wi-Fi network, and make sure that the interface is on.",
128
+ predicts: {
129
+ "link.state": { expect: "down", strength: "sure" },
130
+ "gateway.reply": { expect: "none", strength: "sure" },
131
+ "dns.answer": { expect: "timeout", strength: "strong" },
132
+ "icmp.reply": { expect: ["none", "unreachable"], strength: "sure" },
133
+ "tcp.connect": { expect: ["unreachable", "timeout"], strength: "sure" },
134
+ },
135
+ },
136
+ {
137
+ id: "wifi.weak",
138
+ part: "wifi",
139
+ reach: "shared",
140
+ summary: "The Wi-Fi signal is weak, so the link loses packets.",
141
+ step: "Move nearer to the access point, or use a cable.",
142
+ predicts: {
143
+ "wifi.signal": { expect: "weak", strength: "strong" },
144
+ "path.loss": { expect: ["some", "high"], strength: "usual" },
145
+ "tcp.time": { expect: ["long", "retry"], strength: "usual" },
146
+ },
147
+ },
148
+ {
149
+ id: "gateway.down",
150
+ part: "gateway",
151
+ reach: "shared",
152
+ summary: "The gateway does not forward packets.",
153
+ step: "Restart the router, or examine the link from the router to the provider.",
154
+ predicts: {
155
+ "gateway.reply": { expect: "none", strength: "strong" },
156
+ "dns.answer": { expect: "timeout", strength: "strong" },
157
+ "icmp.reply": { expect: ["none", "unreachable"], strength: "strong" },
158
+ "tcp.connect": { expect: ["timeout", "unreachable"], strength: "strong" },
159
+ },
160
+ },
161
+ {
162
+ id: "captive.portal",
163
+ part: "gateway",
164
+ reach: "shared",
165
+ summary: "A captive portal holds the traffic until you sign in.",
166
+ step: "Open a web page in a browser, and accept the terms of the network.",
167
+ predicts: {
168
+ "dns.address": { expect: "private", strength: "usual" },
169
+ "tls.handshake": { expect: "cert-error", strength: "strong" },
170
+ "tls.trust": { expect: "private", strength: "usual" },
171
+ },
172
+ },
173
+ {
174
+ id: "resolver.down",
175
+ part: "resolver",
176
+ reach: "shared",
177
+ summary: "The DNS resolver does not answer.",
178
+ step: "Use a different DNS resolver, or examine the resolver of the router.",
179
+ predicts: {
180
+ "dns.answer": { expect: "timeout", strength: "strong" },
181
+ },
182
+ },
183
+ {
184
+ id: "resolver.failing",
185
+ part: "resolver",
186
+ reach: "shared",
187
+ summary: "The DNS resolver answers with an error for each name.",
188
+ step: "Use a different DNS resolver, and examine the DNSSEC settings of the resolver.",
189
+ predicts: {
190
+ "dns.answer": { expect: ["servfail", "refused"], strength: "strong" },
191
+ },
192
+ },
193
+ {
194
+ id: "name.missing",
195
+ part: "service",
196
+ reach: "target",
197
+ summary: "The name does not exist in DNS.",
198
+ step: "Examine the spelling of the name, and the records of its DNS zone.",
199
+ predicts: {
200
+ "dns.answer": { expect: "nxdomain", strength: "strong" },
201
+ },
202
+ },
203
+ {
204
+ id: "port.closed",
205
+ part: "service",
206
+ reach: "target",
207
+ summary: "No program listens on the port.",
208
+ step: "Start the service, or use the correct port.",
209
+ predicts: {
210
+ "tcp.connect": { expect: "refused", strength: "strong" },
211
+ },
212
+ },
213
+ {
214
+ id: "host.down",
215
+ part: "service",
216
+ reach: "target",
217
+ summary: "The host is down.",
218
+ step: "Start the host, or examine its network link.",
219
+ predicts: {
220
+ "tcp.connect": { expect: ["timeout", "unreachable"], strength: "strong" },
221
+ "icmp.reply": { expect: ["none", "unreachable"], strength: "strong" },
222
+ },
223
+ },
224
+ {
225
+ id: "filter.drop",
226
+ part: "path",
227
+ reach: "target",
228
+ summary: "A filter drops the packets to the port.",
229
+ step: "Examine the firewall rules of the host and of each network on the path.",
230
+ predicts: {
231
+ "tcp.connect": { expect: "timeout", strength: "strong" },
232
+ "icmp.reply": { reply: 0.5, none: 0.45, unreachable: 0.05 },
233
+ },
234
+ },
235
+ {
236
+ id: "filter.icmp",
237
+ part: "path",
238
+ reach: "target",
239
+ summary: "A filter blocks ICMP. The host is up.",
240
+ step: "No step is necessary for the service. Use TCP to examine the host.",
241
+ predicts: {
242
+ "icmp.reply": { expect: "none", strength: "strong" },
243
+ },
244
+ },
245
+ {
246
+ id: "filter.reset",
247
+ part: "path",
248
+ reach: "target",
249
+ summary: "A device on the path resets the connections.",
250
+ step: "Examine the firewalls and the filters of content on the path, and try a different network.",
251
+ predicts: {
252
+ "tcp.connect": { connected: 0.5, refused: 0.02, timeout: 0.02, unreachable: 0.02, reset: 0.44 },
253
+ "tls.handshake": { expect: "reset", strength: "strong" },
254
+ },
255
+ },
256
+ {
257
+ id: "path.loss",
258
+ part: "path",
259
+ reach: "target",
260
+ summary: "A network on the path loses packets.",
261
+ step: "Find the first hop with loss, and tell the provider of that network.",
262
+ predicts: {
263
+ "path.loss": { expect: ["some", "high"], strength: "strong" },
264
+ "tcp.time": { expect: ["long", "retry"], strength: "usual" },
265
+ "http.ttfb": { expect: ["mid", "high"], strength: "weak" },
266
+ },
267
+ },
268
+ {
269
+ id: "path.mtu",
270
+ part: "path",
271
+ reach: "shared",
272
+ summary: "Large packets do not pass, and no ICMP message tells the size (an MTU black hole).",
273
+ step: "Lower the MTU of the interface or of the VPN, or enable MSS clamping on the router.",
274
+ predicts: {
275
+ "tls.handshake": { expect: "timeout", strength: "strong" },
276
+ "http.status": { expect: "none", strength: "usual" },
277
+ },
278
+ },
279
+ {
280
+ id: "ipv6.broken",
281
+ part: "path",
282
+ reach: "shared",
283
+ summary: "IPv6 does not work, and IPv4 works.",
284
+ step: "Examine the IPv6 settings of the router, or turn off IPv6 until the provider repairs it.",
285
+ predicts: {
286
+ "ip.family": { expect: "ipv4-only", strength: "strong" },
287
+ "tcp.time": { expect: ["long", "retry"], strength: "weak" },
288
+ },
289
+ },
290
+ {
291
+ id: "tls.proxy",
292
+ part: "path",
293
+ reach: "shared",
294
+ summary: "A proxy examines the TLS traffic.",
295
+ step: "Compare the issuer of the certificate with the issuer on a different network, and ask the owner of the network.",
296
+ predicts: {
297
+ "tls.ratio": { expect: ["mid", "high"], strength: "usual" },
298
+ "tls.trust": { expect: "private", strength: "usual" },
299
+ "tcp.time": { expect: "short", strength: "strong" },
300
+ },
301
+ },
302
+ {
303
+ id: "tls.cert-invalid",
304
+ part: "service",
305
+ reach: "target",
306
+ summary: "The certificate of the server is not valid: it expired, or it is for a different name.",
307
+ step: "Renew the certificate of the server, or correct its names.",
308
+ predicts: {
309
+ "tls.handshake": { expect: "cert-error", strength: "strong" },
310
+ },
311
+ },
312
+ {
313
+ id: "tls.mismatch",
314
+ part: "service",
315
+ reach: "target",
316
+ summary: "The client and the server have no TLS version or cipher in common.",
317
+ step: "Enable TLS 1.2 or TLS 1.3 with modern ciphers on the server.",
318
+ predicts: {
319
+ "tls.handshake": { expect: "alert", strength: "strong" },
320
+ },
321
+ },
322
+ {
323
+ id: "server.slow",
324
+ part: "service",
325
+ reach: "target",
326
+ summary: "The server is slow.",
327
+ step: "Examine the load and the logs of the server.",
328
+ predicts: {
329
+ "http.ttfb": { expect: "high", strength: "strong" },
330
+ "tls.ratio": { expect: ["mid", "high"], strength: "weak" },
331
+ },
332
+ },
333
+ {
334
+ id: "server.error",
335
+ part: "service",
336
+ reach: "target",
337
+ summary: "The server answers with an error.",
338
+ step: "Examine the logs of the server and of its upstream services.",
339
+ predicts: {
340
+ "http.status": { expect: "server-error", strength: "strong" },
341
+ },
342
+ },
343
+ ];
344
+
345
+ /** The standard catalog: the rules of practice as the first likelihoods. Each hypothesis has the prior weight 1. */
346
+ export const STANDARD_CATALOG_SPEC: CatalogSpec = {
347
+ version: "1",
348
+ features: STANDARD_FEATURES,
349
+ faults: STANDARD_FAULTS,
350
+ };
351
+
352
+ /** The standard catalog, compiled. */
353
+ export const STANDARD_CATALOG: Catalog = (() => {
354
+ const compiled = compileCatalog(STANDARD_CATALOG_SPEC);
355
+ if (compiled.tag === "err") throw new Error(`The standard catalog has problems: ${compiled.error.join(" ")}`);
356
+ return compiled.value;
357
+ })();
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The diagnosis of netdiag: the model of the parts, the catalog of faults, and the probability of each cause.
3
+ *
4
+ * This entry imports no `node:` module, so a browser can also use it.
5
+ */
6
+ export {
7
+ FLOOR,
8
+ NONE,
9
+ PARTS,
10
+ STRENGTHS,
11
+ UNKNOWN,
12
+ compileCatalog,
13
+ diagnose,
14
+ expand,
15
+ explain,
16
+ predictive,
17
+ type Catalog,
18
+ type CatalogSpec,
19
+ type Cause,
20
+ type Contribution,
21
+ type Diagnosis,
22
+ type Distribution,
23
+ type Evidence,
24
+ type Expectation,
25
+ type Fault,
26
+ type Feature,
27
+ type Hypothesis,
28
+ type Part,
29
+ type Prediction,
30
+ type Reach,
31
+ type Strength,
32
+ } from "./model.ts";
33
+ export { STANDARD_CATALOG, STANDARD_CATALOG_SPEC, STANDARD_FAULTS, STANDARD_FEATURES } from "./catalog.ts";
34
+ export { LAYERS, isLayer, layerRank, type Layer } from "../layer.ts";