@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.
- package/LICENSE +21 -0
- package/README.md +91 -1
- package/build/diagnosis-BTa-Dfwl.js +820 -0
- package/build/diagnosis-BTa-Dfwl.js.map +1 -0
- package/build/diagnosis.d.ts +7 -0
- package/build/diagnosis.js +1 -0
- package/build/index-CP1hzELI.d.ts +198 -0
- package/build/index-CP1hzELI.d.ts.map +1 -0
- package/build/index.d.ts +287 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +436 -0
- package/build/index.js.map +1 -0
- package/package.json +63 -4
- package/src/diagnosis/catalog.ts +357 -0
- package/src/diagnosis/index.ts +34 -0
- package/src/diagnosis/model.ts +322 -0
- package/src/fact.ts +87 -0
- package/src/index.ts +33 -0
- package/src/layer.ts +31 -0
- package/src/probe.ts +61 -0
- package/src/run.ts +163 -0
- package/src/scope.ts +171 -0
- package/src/target.ts +116 -0
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
import { err, ok } from "@mark1russell7/optional";
|
|
2
|
+
// src/layer.ts
|
|
3
|
+
/**
|
|
4
|
+
* The layers of the network stack that netdiag examines, from the bottom to the top.
|
|
5
|
+
*
|
|
6
|
+
* - `link`: the network interface and its physical link.
|
|
7
|
+
* - `wifi`: the Wi-Fi link: the signal, the channel and the rate.
|
|
8
|
+
* - `ip`: the addresses of the interfaces.
|
|
9
|
+
* - `route`: the routes of this computer and its gateway.
|
|
10
|
+
* - `path`: the hops through the provider networks to the remote host.
|
|
11
|
+
* - `dns`: the resolution of names to addresses.
|
|
12
|
+
* - `tcp`: the connections to a port.
|
|
13
|
+
* - `tls`: the handshake and the certificate of the server.
|
|
14
|
+
* - `http`: the request and the response.
|
|
15
|
+
*/
|
|
16
|
+
const LAYERS = [
|
|
17
|
+
"link",
|
|
18
|
+
"wifi",
|
|
19
|
+
"ip",
|
|
20
|
+
"route",
|
|
21
|
+
"path",
|
|
22
|
+
"dns",
|
|
23
|
+
"tcp",
|
|
24
|
+
"tls",
|
|
25
|
+
"http"
|
|
26
|
+
];
|
|
27
|
+
const RANKS = new Map(LAYERS.map((layer, rank) => [layer, rank]));
|
|
28
|
+
/** This function tells if a value is the name of a layer. */
|
|
29
|
+
function isLayer(value) {
|
|
30
|
+
return typeof value === "string" && RANKS.has(value);
|
|
31
|
+
}
|
|
32
|
+
/** This function gives the position of a layer in `LAYERS`. The bottom layer `link` has the position 0. */
|
|
33
|
+
function layerRank(layer) {
|
|
34
|
+
const rank = RANKS.get(layer);
|
|
35
|
+
if (rank === void 0) throw new RangeError(`"${String(layer)}" is not a layer`);
|
|
36
|
+
return rank;
|
|
37
|
+
}
|
|
38
|
+
// src/diagnosis/model.ts
|
|
39
|
+
/** The parts of the model. Each fault is in one part. */
|
|
40
|
+
const PARTS = [
|
|
41
|
+
"interface",
|
|
42
|
+
"wifi",
|
|
43
|
+
"gateway",
|
|
44
|
+
"resolver",
|
|
45
|
+
"path",
|
|
46
|
+
"service"
|
|
47
|
+
];
|
|
48
|
+
/** The probability that each strength gives to the expected values together. */
|
|
49
|
+
const STRENGTHS = {
|
|
50
|
+
sure: .98,
|
|
51
|
+
strong: .9,
|
|
52
|
+
usual: .75,
|
|
53
|
+
weak: .6
|
|
54
|
+
};
|
|
55
|
+
function isExpectation(prediction) {
|
|
56
|
+
return typeof prediction.strength === "string";
|
|
57
|
+
}
|
|
58
|
+
/** The id of the hypothesis "no fault". */
|
|
59
|
+
const NONE = "none";
|
|
60
|
+
/** The id of the hypothesis "unknown cause". */
|
|
61
|
+
const UNKNOWN = "unknown";
|
|
62
|
+
/** The lowest probability of a value. With a probability of 0, one wrong observation removes a cause. */
|
|
63
|
+
const FLOOR = .001;
|
|
64
|
+
const SUM_TOLERANCE = 1e-9;
|
|
65
|
+
function distributionProblems(where, feature, dist) {
|
|
66
|
+
const problems = [];
|
|
67
|
+
for (const value of Object.keys(dist)) if (!feature.values.includes(value)) problems.push(`${where}: "${value}" is not a value of ${feature.id}.`);
|
|
68
|
+
for (const value of feature.values) {
|
|
69
|
+
const p = dist[value];
|
|
70
|
+
if (p === void 0) problems.push(`${where}: the value "${value}" of ${feature.id} has no probability.`);
|
|
71
|
+
else if (!Number.isFinite(p) || p < .001) problems.push(`${where}: the probability of "${value}" is ${p}. It must be ${FLOOR} or more.`);
|
|
72
|
+
}
|
|
73
|
+
const sum = Object.values(dist).reduce((a, b) => a + b, 0);
|
|
74
|
+
if (Math.abs(sum - 1) > SUM_TOLERANCE) problems.push(`${where}: the sum of the probabilities is ${sum}, not 1.`);
|
|
75
|
+
return problems;
|
|
76
|
+
}
|
|
77
|
+
/** This function gives the full distribution of a prediction, or the problems of the prediction. */
|
|
78
|
+
function expand(feature, prediction) {
|
|
79
|
+
if (!isExpectation(prediction)) return ok(prediction);
|
|
80
|
+
const expected = typeof prediction.expect === "string" ? [prediction.expect] : [...prediction.expect];
|
|
81
|
+
const unknown = expected.filter((value) => !feature.values.includes(value));
|
|
82
|
+
if (unknown.length > 0) return err(unknown.map((value) => `"${value}" is not a value of ${feature.id}.`));
|
|
83
|
+
if (expected.length === 0 || new Set(expected).size !== expected.length) return err([`The expected values of ${feature.id} are empty or repeated.`]);
|
|
84
|
+
const others = feature.values.filter((value) => !expected.includes(value));
|
|
85
|
+
const mass = others.length === 0 ? 1 : STRENGTHS[prediction.strength];
|
|
86
|
+
const dist = {};
|
|
87
|
+
for (const value of expected) dist[value] = mass / expected.length;
|
|
88
|
+
for (const value of others) dist[value] = (1 - mass) / others.length;
|
|
89
|
+
return ok(dist);
|
|
90
|
+
}
|
|
91
|
+
const ID = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
|
|
92
|
+
/**
|
|
93
|
+
* This function examines a catalog and makes its tables.
|
|
94
|
+
*
|
|
95
|
+
* It refuses a repeated id, an unknown feature or value, a probability below `FLOOR`, and a distribution with a sum that is not 1.
|
|
96
|
+
*/
|
|
97
|
+
function compileCatalog(spec) {
|
|
98
|
+
const problems = [];
|
|
99
|
+
const features = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const feature of spec.features) {
|
|
101
|
+
if (!ID.test(feature.id)) problems.push(`"${feature.id}" is not a valid feature id.`);
|
|
102
|
+
if (features.has(feature.id)) problems.push(`The feature ${feature.id} is repeated.`);
|
|
103
|
+
if (feature.values.length < 2 || new Set(feature.values).size !== feature.values.length) problems.push(`The feature ${feature.id} must have two or more different values.`);
|
|
104
|
+
problems.push(...distributionProblems(`healthy of ${feature.id}`, feature, feature.healthy));
|
|
105
|
+
features.set(feature.id, feature);
|
|
106
|
+
}
|
|
107
|
+
const hypotheses = [{
|
|
108
|
+
id: NONE,
|
|
109
|
+
kind: "none",
|
|
110
|
+
summary: "The network has no fault.",
|
|
111
|
+
step: "No step is necessary."
|
|
112
|
+
}];
|
|
113
|
+
const priors = /* @__PURE__ */ new Map([[NONE, spec.priors?.none ?? 1]]);
|
|
114
|
+
const tables = /* @__PURE__ */ new Map();
|
|
115
|
+
for (const fault of spec.faults) {
|
|
116
|
+
if (!ID.test(fault.id) || fault.id === "none" || fault.id === "unknown") problems.push(`"${fault.id}" is not a valid fault id.`);
|
|
117
|
+
if (priors.has(fault.id)) problems.push(`The fault ${fault.id} is repeated.`);
|
|
118
|
+
priors.set(fault.id, fault.prior ?? 1);
|
|
119
|
+
hypotheses.push({
|
|
120
|
+
id: fault.id,
|
|
121
|
+
kind: "fault",
|
|
122
|
+
summary: fault.summary,
|
|
123
|
+
step: fault.step,
|
|
124
|
+
part: fault.part,
|
|
125
|
+
reach: fault.reach
|
|
126
|
+
});
|
|
127
|
+
const table = /* @__PURE__ */ new Map();
|
|
128
|
+
for (const [featureId, prediction] of Object.entries(fault.predicts)) {
|
|
129
|
+
const feature = features.get(featureId);
|
|
130
|
+
if (feature === void 0) {
|
|
131
|
+
problems.push(`The fault ${fault.id} predicts the unknown feature ${featureId}.`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const dist = expand(feature, prediction);
|
|
135
|
+
if (dist.tag === "err") problems.push(...dist.error.map((problem) => `${fault.id}: ${problem}`));
|
|
136
|
+
else {
|
|
137
|
+
problems.push(...distributionProblems(`${fault.id} for ${featureId}`, feature, dist.value));
|
|
138
|
+
table.set(featureId, dist.value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (table.size === 0) problems.push(`The fault ${fault.id} predicts no feature.`);
|
|
142
|
+
tables.set(fault.id, table);
|
|
143
|
+
}
|
|
144
|
+
hypotheses.push({
|
|
145
|
+
id: UNKNOWN,
|
|
146
|
+
kind: "unknown",
|
|
147
|
+
summary: "The catalog does not have the cause.",
|
|
148
|
+
step: "Collect more facts, and report the facts as a new fault of the catalog."
|
|
149
|
+
});
|
|
150
|
+
priors.set(UNKNOWN, spec.priors?.unknown ?? 1);
|
|
151
|
+
for (const [id, prior] of priors) if (!Number.isFinite(prior) || prior <= 0) problems.push(`The prior of ${id} is ${prior}. It must be more than 0.`);
|
|
152
|
+
if (problems.length > 0) return err(problems);
|
|
153
|
+
const total = [...priors.values()].reduce((a, b) => a + b, 0);
|
|
154
|
+
const reach = new Map(hypotheses.map((h) => [h.id, h.reach]));
|
|
155
|
+
return ok({
|
|
156
|
+
version: spec.version,
|
|
157
|
+
features,
|
|
158
|
+
hypotheses,
|
|
159
|
+
prior: (hypothesis) => (priors.get(hypothesis) ?? 0) / total,
|
|
160
|
+
likelihood(hypothesis, featureId, value, primary) {
|
|
161
|
+
const feature = features.get(featureId);
|
|
162
|
+
if (feature === void 0 || !feature.values.includes(value)) return NaN;
|
|
163
|
+
if (hypothesis === "unknown") return 1 / feature.values.length;
|
|
164
|
+
const changed = tables.get(hypothesis)?.get(featureId);
|
|
165
|
+
return ((primary || reach.get(hypothesis) === "shared") && changed !== void 0 ? changed : feature.healthy)[value] ?? NaN;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function usable(catalog, evidence) {
|
|
170
|
+
const used = [];
|
|
171
|
+
const ignored = [];
|
|
172
|
+
for (const item of evidence) {
|
|
173
|
+
const feature = catalog.features.get(item.feature);
|
|
174
|
+
if (feature === void 0) ignored.push({
|
|
175
|
+
evidence: item,
|
|
176
|
+
reason: `The catalog has no feature ${item.feature}.`
|
|
177
|
+
});
|
|
178
|
+
else if (!feature.values.includes(item.value)) ignored.push({
|
|
179
|
+
evidence: item,
|
|
180
|
+
reason: `"${item.value}" is not a value of ${item.feature}.`
|
|
181
|
+
});
|
|
182
|
+
else used.push(item);
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
used,
|
|
186
|
+
ignored
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* This function gives the probability of each hypothesis from the evidence, with the rule of Bayes.
|
|
191
|
+
*
|
|
192
|
+
* The model takes the observations as independent for a given hypothesis. The calculation uses logarithms, so a long
|
|
193
|
+
* list of evidence does not go to zero. The order of the evidence does not change the result.
|
|
194
|
+
* An exact tie keeps the order of the catalog, with "no fault" first.
|
|
195
|
+
*/
|
|
196
|
+
function diagnose(catalog, evidence) {
|
|
197
|
+
const { used, ignored } = usable(catalog, evidence);
|
|
198
|
+
const logs = catalog.hypotheses.map((h) => {
|
|
199
|
+
let sum = Math.log(catalog.prior(h.id));
|
|
200
|
+
for (const item of used) sum += Math.log(catalog.likelihood(h.id, item.feature, item.value, item.primary));
|
|
201
|
+
return sum;
|
|
202
|
+
});
|
|
203
|
+
const top = Math.max(...logs);
|
|
204
|
+
const weights = logs.map((log) => Math.exp(log - top));
|
|
205
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
206
|
+
return {
|
|
207
|
+
causes: catalog.hypotheses.map((h, i) => ({
|
|
208
|
+
...h,
|
|
209
|
+
probability: (weights[i] ?? 0) / total,
|
|
210
|
+
order: i
|
|
211
|
+
})).sort((a, b) => b.probability - a.probability || a.order - b.order).map(({ order: _order, ...cause }) => cause),
|
|
212
|
+
used,
|
|
213
|
+
ignored
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* This function tells why the model prefers one hypothesis to a second hypothesis.
|
|
218
|
+
*
|
|
219
|
+
* It gives each observation with its weight, from the largest weight in absolute value. The default second hypothesis is
|
|
220
|
+
* "no fault".
|
|
221
|
+
*/
|
|
222
|
+
function explain(catalog, evidence, hypothesis, versus = NONE) {
|
|
223
|
+
const { used } = usable(catalog, evidence);
|
|
224
|
+
return used.map((item) => ({
|
|
225
|
+
evidence: item,
|
|
226
|
+
weight: Math.log10(catalog.likelihood(hypothesis, item.feature, item.value, item.primary)) - Math.log10(catalog.likelihood(versus, item.feature, item.value, item.primary))
|
|
227
|
+
})).sort((a, b) => Math.abs(b.weight) - Math.abs(a.weight));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* This function gives the probability of each value of a feature that is not observed yet, from the probabilities of
|
|
231
|
+
* the causes. The choice of the next probe uses it.
|
|
232
|
+
*/
|
|
233
|
+
function predictive(catalog, diagnosis, feature, primary) {
|
|
234
|
+
const known = catalog.features.get(feature);
|
|
235
|
+
if (known === void 0) throw new RangeError(`The catalog has no feature ${feature}`);
|
|
236
|
+
const dist = {};
|
|
237
|
+
for (const value of known.values) dist[value] = diagnosis.causes.reduce((sum, cause) => sum + cause.probability * catalog.likelihood(cause.id, feature, value, primary), 0);
|
|
238
|
+
return dist;
|
|
239
|
+
}
|
|
240
|
+
// src/diagnosis/catalog.ts
|
|
241
|
+
/**
|
|
242
|
+
* The features of the standard catalog.
|
|
243
|
+
*
|
|
244
|
+
* The readers of the facts give these features. The distribution `healthy` of each feature is a rule of practice. The
|
|
245
|
+
* lab runs (ND-7) then fit it.
|
|
246
|
+
*/
|
|
247
|
+
const STANDARD_FEATURES = [
|
|
248
|
+
{
|
|
249
|
+
id: "link.state",
|
|
250
|
+
layer: "link",
|
|
251
|
+
summary: "The state of the link of the network interface.",
|
|
252
|
+
values: ["up", "down"],
|
|
253
|
+
healthy: {
|
|
254
|
+
up: .999,
|
|
255
|
+
down: .001
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: "wifi.signal",
|
|
260
|
+
layer: "wifi",
|
|
261
|
+
summary: "The strength of the Wi-Fi signal: strong above -60 dBm, weak below -75 dBm.",
|
|
262
|
+
values: [
|
|
263
|
+
"strong",
|
|
264
|
+
"fair",
|
|
265
|
+
"weak"
|
|
266
|
+
],
|
|
267
|
+
healthy: {
|
|
268
|
+
strong: .7,
|
|
269
|
+
fair: .25,
|
|
270
|
+
weak: .05
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: "gateway.reply",
|
|
275
|
+
layer: "route",
|
|
276
|
+
summary: "The gateway answers an ICMP echo request.",
|
|
277
|
+
values: ["reply", "none"],
|
|
278
|
+
healthy: {
|
|
279
|
+
reply: .97,
|
|
280
|
+
none: .03
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: "path.loss",
|
|
285
|
+
layer: "path",
|
|
286
|
+
summary: "The packet loss on the path: none, some (up to 5 %) or high (more than 5 %).",
|
|
287
|
+
values: [
|
|
288
|
+
"none",
|
|
289
|
+
"some",
|
|
290
|
+
"high"
|
|
291
|
+
],
|
|
292
|
+
healthy: {
|
|
293
|
+
none: .9,
|
|
294
|
+
some: .08,
|
|
295
|
+
high: .02
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: "dns.answer",
|
|
300
|
+
layer: "dns",
|
|
301
|
+
summary: "The result of the DNS lookup of the name of the target.",
|
|
302
|
+
values: [
|
|
303
|
+
"ok",
|
|
304
|
+
"nxdomain",
|
|
305
|
+
"servfail",
|
|
306
|
+
"refused",
|
|
307
|
+
"timeout"
|
|
308
|
+
],
|
|
309
|
+
healthy: {
|
|
310
|
+
ok: .97,
|
|
311
|
+
nxdomain: .01,
|
|
312
|
+
servfail: .005,
|
|
313
|
+
refused: .005,
|
|
314
|
+
timeout: .01
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: "dns.address",
|
|
319
|
+
layer: "dns",
|
|
320
|
+
summary: "The kind of the addresses of a public name: public, or private (RFC 1918, RFC 4193 or loopback).",
|
|
321
|
+
values: ["public", "private"],
|
|
322
|
+
healthy: {
|
|
323
|
+
public: .95,
|
|
324
|
+
private: .05
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: "ip.family",
|
|
329
|
+
layer: "ip",
|
|
330
|
+
summary: "The address families that connect, for a host with IPv4 and IPv6 addresses.",
|
|
331
|
+
values: [
|
|
332
|
+
"both",
|
|
333
|
+
"ipv4-only",
|
|
334
|
+
"ipv6-only"
|
|
335
|
+
],
|
|
336
|
+
healthy: {
|
|
337
|
+
both: .9,
|
|
338
|
+
"ipv4-only": .08,
|
|
339
|
+
"ipv6-only": .02
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: "icmp.reply",
|
|
344
|
+
layer: "ip",
|
|
345
|
+
summary: "The host answers an ICMP echo request, does not answer, or a router tells that it is unreachable.",
|
|
346
|
+
values: [
|
|
347
|
+
"reply",
|
|
348
|
+
"none",
|
|
349
|
+
"unreachable"
|
|
350
|
+
],
|
|
351
|
+
healthy: {
|
|
352
|
+
reply: .85,
|
|
353
|
+
none: .14,
|
|
354
|
+
unreachable: .01
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: "tcp.connect",
|
|
359
|
+
layer: "tcp",
|
|
360
|
+
summary: "The outcome of a TCP connect to the port of the target.",
|
|
361
|
+
values: [
|
|
362
|
+
"connected",
|
|
363
|
+
"refused",
|
|
364
|
+
"timeout",
|
|
365
|
+
"unreachable",
|
|
366
|
+
"reset"
|
|
367
|
+
],
|
|
368
|
+
healthy: {
|
|
369
|
+
connected: .97,
|
|
370
|
+
refused: .01,
|
|
371
|
+
timeout: .01,
|
|
372
|
+
unreachable: .005,
|
|
373
|
+
reset: .005
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
id: "tcp.time",
|
|
378
|
+
layer: "tcp",
|
|
379
|
+
summary: "The time of the TCP connect: short (below 150 ms), long, or retry (900 ms or more, a SYN sent again).",
|
|
380
|
+
values: [
|
|
381
|
+
"short",
|
|
382
|
+
"long",
|
|
383
|
+
"retry"
|
|
384
|
+
],
|
|
385
|
+
healthy: {
|
|
386
|
+
short: .8,
|
|
387
|
+
long: .18,
|
|
388
|
+
retry: .02
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
id: "tls.handshake",
|
|
393
|
+
layer: "tls",
|
|
394
|
+
summary: "The outcome of the TLS handshake.",
|
|
395
|
+
values: [
|
|
396
|
+
"ok",
|
|
397
|
+
"cert-error",
|
|
398
|
+
"alert",
|
|
399
|
+
"timeout",
|
|
400
|
+
"reset"
|
|
401
|
+
],
|
|
402
|
+
healthy: {
|
|
403
|
+
ok: .97,
|
|
404
|
+
"cert-error": .01,
|
|
405
|
+
alert: .01,
|
|
406
|
+
timeout: .005,
|
|
407
|
+
reset: .005
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
id: "tls.ratio",
|
|
412
|
+
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).",
|
|
414
|
+
values: [
|
|
415
|
+
"low",
|
|
416
|
+
"mid",
|
|
417
|
+
"high"
|
|
418
|
+
],
|
|
419
|
+
healthy: {
|
|
420
|
+
low: .75,
|
|
421
|
+
mid: .2,
|
|
422
|
+
high: .05
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
id: "tls.trust",
|
|
427
|
+
layer: "tls",
|
|
428
|
+
summary: "The issuer of the certificate: a public root of trust, or a private root.",
|
|
429
|
+
values: ["public", "private"],
|
|
430
|
+
healthy: {
|
|
431
|
+
public: .97,
|
|
432
|
+
private: .03
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
id: "http.status",
|
|
437
|
+
layer: "http",
|
|
438
|
+
summary: "The class of the HTTP status: ok (2xx and 3xx), client-error (4xx), server-error (5xx), or none.",
|
|
439
|
+
values: [
|
|
440
|
+
"ok",
|
|
441
|
+
"client-error",
|
|
442
|
+
"server-error",
|
|
443
|
+
"none"
|
|
444
|
+
],
|
|
445
|
+
healthy: {
|
|
446
|
+
ok: .95,
|
|
447
|
+
"client-error": .03,
|
|
448
|
+
"server-error": .01,
|
|
449
|
+
none: .01
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
id: "http.ttfb",
|
|
454
|
+
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).",
|
|
456
|
+
values: [
|
|
457
|
+
"low",
|
|
458
|
+
"mid",
|
|
459
|
+
"high"
|
|
460
|
+
],
|
|
461
|
+
healthy: {
|
|
462
|
+
low: .6,
|
|
463
|
+
mid: .3,
|
|
464
|
+
high: .1
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
];
|
|
468
|
+
/**
|
|
469
|
+
* The faults of the standard catalog. Each prediction is a rule of practice. The lab runs (ND-7) then fit the
|
|
470
|
+
* distributions, and the priors.
|
|
471
|
+
*/
|
|
472
|
+
const STANDARD_FAULTS = [
|
|
473
|
+
{
|
|
474
|
+
id: "interface.down",
|
|
475
|
+
part: "interface",
|
|
476
|
+
reach: "shared",
|
|
477
|
+
summary: "The network interface has no link.",
|
|
478
|
+
step: "Connect the cable or the Wi-Fi network, and make sure that the interface is on.",
|
|
479
|
+
predicts: {
|
|
480
|
+
"link.state": {
|
|
481
|
+
expect: "down",
|
|
482
|
+
strength: "sure"
|
|
483
|
+
},
|
|
484
|
+
"gateway.reply": {
|
|
485
|
+
expect: "none",
|
|
486
|
+
strength: "sure"
|
|
487
|
+
},
|
|
488
|
+
"dns.answer": {
|
|
489
|
+
expect: "timeout",
|
|
490
|
+
strength: "strong"
|
|
491
|
+
},
|
|
492
|
+
"icmp.reply": {
|
|
493
|
+
expect: ["none", "unreachable"],
|
|
494
|
+
strength: "sure"
|
|
495
|
+
},
|
|
496
|
+
"tcp.connect": {
|
|
497
|
+
expect: ["unreachable", "timeout"],
|
|
498
|
+
strength: "sure"
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
id: "wifi.weak",
|
|
504
|
+
part: "wifi",
|
|
505
|
+
reach: "shared",
|
|
506
|
+
summary: "The Wi-Fi signal is weak, so the link loses packets.",
|
|
507
|
+
step: "Move nearer to the access point, or use a cable.",
|
|
508
|
+
predicts: {
|
|
509
|
+
"wifi.signal": {
|
|
510
|
+
expect: "weak",
|
|
511
|
+
strength: "strong"
|
|
512
|
+
},
|
|
513
|
+
"path.loss": {
|
|
514
|
+
expect: ["some", "high"],
|
|
515
|
+
strength: "usual"
|
|
516
|
+
},
|
|
517
|
+
"tcp.time": {
|
|
518
|
+
expect: ["long", "retry"],
|
|
519
|
+
strength: "usual"
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
id: "gateway.down",
|
|
525
|
+
part: "gateway",
|
|
526
|
+
reach: "shared",
|
|
527
|
+
summary: "The gateway does not forward packets.",
|
|
528
|
+
step: "Restart the router, or examine the link from the router to the provider.",
|
|
529
|
+
predicts: {
|
|
530
|
+
"gateway.reply": {
|
|
531
|
+
expect: "none",
|
|
532
|
+
strength: "strong"
|
|
533
|
+
},
|
|
534
|
+
"dns.answer": {
|
|
535
|
+
expect: "timeout",
|
|
536
|
+
strength: "strong"
|
|
537
|
+
},
|
|
538
|
+
"icmp.reply": {
|
|
539
|
+
expect: ["none", "unreachable"],
|
|
540
|
+
strength: "strong"
|
|
541
|
+
},
|
|
542
|
+
"tcp.connect": {
|
|
543
|
+
expect: ["timeout", "unreachable"],
|
|
544
|
+
strength: "strong"
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
id: "captive.portal",
|
|
550
|
+
part: "gateway",
|
|
551
|
+
reach: "shared",
|
|
552
|
+
summary: "A captive portal holds the traffic until you sign in.",
|
|
553
|
+
step: "Open a web page in a browser, and accept the terms of the network.",
|
|
554
|
+
predicts: {
|
|
555
|
+
"dns.address": {
|
|
556
|
+
expect: "private",
|
|
557
|
+
strength: "usual"
|
|
558
|
+
},
|
|
559
|
+
"tls.handshake": {
|
|
560
|
+
expect: "cert-error",
|
|
561
|
+
strength: "strong"
|
|
562
|
+
},
|
|
563
|
+
"tls.trust": {
|
|
564
|
+
expect: "private",
|
|
565
|
+
strength: "usual"
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
id: "resolver.down",
|
|
571
|
+
part: "resolver",
|
|
572
|
+
reach: "shared",
|
|
573
|
+
summary: "The DNS resolver does not answer.",
|
|
574
|
+
step: "Use a different DNS resolver, or examine the resolver of the router.",
|
|
575
|
+
predicts: { "dns.answer": {
|
|
576
|
+
expect: "timeout",
|
|
577
|
+
strength: "strong"
|
|
578
|
+
} }
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
id: "resolver.failing",
|
|
582
|
+
part: "resolver",
|
|
583
|
+
reach: "shared",
|
|
584
|
+
summary: "The DNS resolver answers with an error for each name.",
|
|
585
|
+
step: "Use a different DNS resolver, and examine the DNSSEC settings of the resolver.",
|
|
586
|
+
predicts: { "dns.answer": {
|
|
587
|
+
expect: ["servfail", "refused"],
|
|
588
|
+
strength: "strong"
|
|
589
|
+
} }
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
id: "name.missing",
|
|
593
|
+
part: "service",
|
|
594
|
+
reach: "target",
|
|
595
|
+
summary: "The name does not exist in DNS.",
|
|
596
|
+
step: "Examine the spelling of the name, and the records of its DNS zone.",
|
|
597
|
+
predicts: { "dns.answer": {
|
|
598
|
+
expect: "nxdomain",
|
|
599
|
+
strength: "strong"
|
|
600
|
+
} }
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
id: "port.closed",
|
|
604
|
+
part: "service",
|
|
605
|
+
reach: "target",
|
|
606
|
+
summary: "No program listens on the port.",
|
|
607
|
+
step: "Start the service, or use the correct port.",
|
|
608
|
+
predicts: { "tcp.connect": {
|
|
609
|
+
expect: "refused",
|
|
610
|
+
strength: "strong"
|
|
611
|
+
} }
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
id: "host.down",
|
|
615
|
+
part: "service",
|
|
616
|
+
reach: "target",
|
|
617
|
+
summary: "The host is down.",
|
|
618
|
+
step: "Start the host, or examine its network link.",
|
|
619
|
+
predicts: {
|
|
620
|
+
"tcp.connect": {
|
|
621
|
+
expect: ["timeout", "unreachable"],
|
|
622
|
+
strength: "strong"
|
|
623
|
+
},
|
|
624
|
+
"icmp.reply": {
|
|
625
|
+
expect: ["none", "unreachable"],
|
|
626
|
+
strength: "strong"
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
id: "filter.drop",
|
|
632
|
+
part: "path",
|
|
633
|
+
reach: "target",
|
|
634
|
+
summary: "A filter drops the packets to the port.",
|
|
635
|
+
step: "Examine the firewall rules of the host and of each network on the path.",
|
|
636
|
+
predicts: {
|
|
637
|
+
"tcp.connect": {
|
|
638
|
+
expect: "timeout",
|
|
639
|
+
strength: "strong"
|
|
640
|
+
},
|
|
641
|
+
"icmp.reply": {
|
|
642
|
+
reply: .5,
|
|
643
|
+
none: .45,
|
|
644
|
+
unreachable: .05
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
id: "filter.icmp",
|
|
650
|
+
part: "path",
|
|
651
|
+
reach: "target",
|
|
652
|
+
summary: "A filter blocks ICMP. The host is up.",
|
|
653
|
+
step: "No step is necessary for the service. Use TCP to examine the host.",
|
|
654
|
+
predicts: { "icmp.reply": {
|
|
655
|
+
expect: "none",
|
|
656
|
+
strength: "strong"
|
|
657
|
+
} }
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
id: "filter.reset",
|
|
661
|
+
part: "path",
|
|
662
|
+
reach: "target",
|
|
663
|
+
summary: "A device on the path resets the connections.",
|
|
664
|
+
step: "Examine the firewalls and the filters of content on the path, and try a different network.",
|
|
665
|
+
predicts: {
|
|
666
|
+
"tcp.connect": {
|
|
667
|
+
connected: .5,
|
|
668
|
+
refused: .02,
|
|
669
|
+
timeout: .02,
|
|
670
|
+
unreachable: .02,
|
|
671
|
+
reset: .44
|
|
672
|
+
},
|
|
673
|
+
"tls.handshake": {
|
|
674
|
+
expect: "reset",
|
|
675
|
+
strength: "strong"
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
id: "path.loss",
|
|
681
|
+
part: "path",
|
|
682
|
+
reach: "target",
|
|
683
|
+
summary: "A network on the path loses packets.",
|
|
684
|
+
step: "Find the first hop with loss, and tell the provider of that network.",
|
|
685
|
+
predicts: {
|
|
686
|
+
"path.loss": {
|
|
687
|
+
expect: ["some", "high"],
|
|
688
|
+
strength: "strong"
|
|
689
|
+
},
|
|
690
|
+
"tcp.time": {
|
|
691
|
+
expect: ["long", "retry"],
|
|
692
|
+
strength: "usual"
|
|
693
|
+
},
|
|
694
|
+
"http.ttfb": {
|
|
695
|
+
expect: ["mid", "high"],
|
|
696
|
+
strength: "weak"
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
id: "path.mtu",
|
|
702
|
+
part: "path",
|
|
703
|
+
reach: "shared",
|
|
704
|
+
summary: "Large packets do not pass, and no ICMP message tells the size (an MTU black hole).",
|
|
705
|
+
step: "Lower the MTU of the interface or of the VPN, or enable MSS clamping on the router.",
|
|
706
|
+
predicts: {
|
|
707
|
+
"tls.handshake": {
|
|
708
|
+
expect: "timeout",
|
|
709
|
+
strength: "strong"
|
|
710
|
+
},
|
|
711
|
+
"http.status": {
|
|
712
|
+
expect: "none",
|
|
713
|
+
strength: "usual"
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
id: "ipv6.broken",
|
|
719
|
+
part: "path",
|
|
720
|
+
reach: "shared",
|
|
721
|
+
summary: "IPv6 does not work, and IPv4 works.",
|
|
722
|
+
step: "Examine the IPv6 settings of the router, or turn off IPv6 until the provider repairs it.",
|
|
723
|
+
predicts: {
|
|
724
|
+
"ip.family": {
|
|
725
|
+
expect: "ipv4-only",
|
|
726
|
+
strength: "strong"
|
|
727
|
+
},
|
|
728
|
+
"tcp.time": {
|
|
729
|
+
expect: ["long", "retry"],
|
|
730
|
+
strength: "weak"
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
id: "tls.proxy",
|
|
736
|
+
part: "path",
|
|
737
|
+
reach: "shared",
|
|
738
|
+
summary: "A proxy examines the TLS traffic.",
|
|
739
|
+
step: "Compare the issuer of the certificate with the issuer on a different network, and ask the owner of the network.",
|
|
740
|
+
predicts: {
|
|
741
|
+
"tls.ratio": {
|
|
742
|
+
expect: ["mid", "high"],
|
|
743
|
+
strength: "usual"
|
|
744
|
+
},
|
|
745
|
+
"tls.trust": {
|
|
746
|
+
expect: "private",
|
|
747
|
+
strength: "usual"
|
|
748
|
+
},
|
|
749
|
+
"tcp.time": {
|
|
750
|
+
expect: "short",
|
|
751
|
+
strength: "strong"
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
id: "tls.cert-invalid",
|
|
757
|
+
part: "service",
|
|
758
|
+
reach: "target",
|
|
759
|
+
summary: "The certificate of the server is not valid: it expired, or it is for a different name.",
|
|
760
|
+
step: "Renew the certificate of the server, or correct its names.",
|
|
761
|
+
predicts: { "tls.handshake": {
|
|
762
|
+
expect: "cert-error",
|
|
763
|
+
strength: "strong"
|
|
764
|
+
} }
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
id: "tls.mismatch",
|
|
768
|
+
part: "service",
|
|
769
|
+
reach: "target",
|
|
770
|
+
summary: "The client and the server have no TLS version or cipher in common.",
|
|
771
|
+
step: "Enable TLS 1.2 or TLS 1.3 with modern ciphers on the server.",
|
|
772
|
+
predicts: { "tls.handshake": {
|
|
773
|
+
expect: "alert",
|
|
774
|
+
strength: "strong"
|
|
775
|
+
} }
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
id: "server.slow",
|
|
779
|
+
part: "service",
|
|
780
|
+
reach: "target",
|
|
781
|
+
summary: "The server is slow.",
|
|
782
|
+
step: "Examine the load and the logs of the server.",
|
|
783
|
+
predicts: {
|
|
784
|
+
"http.ttfb": {
|
|
785
|
+
expect: "high",
|
|
786
|
+
strength: "strong"
|
|
787
|
+
},
|
|
788
|
+
"tls.ratio": {
|
|
789
|
+
expect: ["mid", "high"],
|
|
790
|
+
strength: "weak"
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
id: "server.error",
|
|
796
|
+
part: "service",
|
|
797
|
+
reach: "target",
|
|
798
|
+
summary: "The server answers with an error.",
|
|
799
|
+
step: "Examine the logs of the server and of its upstream services.",
|
|
800
|
+
predicts: { "http.status": {
|
|
801
|
+
expect: "server-error",
|
|
802
|
+
strength: "strong"
|
|
803
|
+
} }
|
|
804
|
+
}
|
|
805
|
+
];
|
|
806
|
+
/** The standard catalog: the rules of practice as the first likelihoods. Each hypothesis has the prior weight 1. */
|
|
807
|
+
const STANDARD_CATALOG_SPEC = {
|
|
808
|
+
version: "1",
|
|
809
|
+
features: STANDARD_FEATURES,
|
|
810
|
+
faults: STANDARD_FAULTS
|
|
811
|
+
};
|
|
812
|
+
/** The standard catalog, compiled. */
|
|
813
|
+
const STANDARD_CATALOG = (() => {
|
|
814
|
+
const compiled = compileCatalog(STANDARD_CATALOG_SPEC);
|
|
815
|
+
if (compiled.tag === "err") throw new Error(`The standard catalog has problems: ${compiled.error.join(" ")}`);
|
|
816
|
+
return compiled.value;
|
|
817
|
+
})();
|
|
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 };
|
|
819
|
+
|
|
820
|
+
//# sourceMappingURL=diagnosis-BTa-Dfwl.js.map
|