@mark1russell7/netdiag 0.0.0-stage → 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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +244 -2
  3. package/build/address.d.ts +56 -0
  4. package/build/address.d.ts.map +1 -0
  5. package/build/address.js +172 -0
  6. package/build/address.js.map +1 -0
  7. package/build/diagnosis-a4hRzFxp.js +922 -0
  8. package/build/diagnosis-a4hRzFxp.js.map +1 -0
  9. package/build/diagnosis.d.ts +8 -0
  10. package/build/diagnosis.js +1 -0
  11. package/build/index-K2dGiYxW.d.ts +259 -0
  12. package/build/index-K2dGiYxW.d.ts.map +1 -0
  13. package/build/index.d.ts +392 -0
  14. package/build/index.d.ts.map +1 -0
  15. package/build/index.js +1359 -0
  16. package/build/index.js.map +1 -0
  17. package/build/kinds-CQv769lK.js +341 -0
  18. package/build/kinds-CQv769lK.js.map +1 -0
  19. package/build/kinds-_NShfZpU.d.ts +377 -0
  20. package/build/kinds-_NShfZpU.d.ts.map +1 -0
  21. package/build/layer-BNg0VYuK.d.ts +22 -0
  22. package/build/layer-BNg0VYuK.d.ts.map +1 -0
  23. package/build/tools.d.ts +206 -0
  24. package/build/tools.d.ts.map +1 -0
  25. package/build/tools.js +896 -0
  26. package/build/tools.js.map +1 -0
  27. package/package.json +81 -3
  28. package/src/address/index.ts +155 -0
  29. package/src/diagnosis/catalog.ts +357 -0
  30. package/src/diagnosis/index.ts +47 -0
  31. package/src/diagnosis/model.ts +322 -0
  32. package/src/diagnosis/score.ts +160 -0
  33. package/src/fact.ts +87 -0
  34. package/src/index.ts +68 -0
  35. package/src/kinds.ts +460 -0
  36. package/src/layer.ts +31 -0
  37. package/src/probe.ts +61 -0
  38. package/src/probes/native.ts +354 -0
  39. package/src/probes/socket.ts +520 -0
  40. package/src/readers.ts +367 -0
  41. package/src/run.ts +163 -0
  42. package/src/scope.ts +152 -0
  43. package/src/target.ts +116 -0
  44. package/src/tools/common.ts +47 -0
  45. package/src/tools/host.ts +91 -0
  46. package/src/tools/index.ts +38 -0
  47. package/src/tools/licenses.ts +37 -0
  48. package/src/tools/mtr.ts +156 -0
  49. package/src/tools/ping.ts +231 -0
  50. package/src/tools/traceroute.ts +313 -0
package/build/index.js ADDED
@@ -0,0 +1,1359 @@
1
+ import { _ as explain, a as STANDARD_CATALOG, b as isLayer, c as STANDARD_FEATURES, d as PARTS, f as STRENGTHS, g as expand, h as diagnose, i as scoreLab, l as FLOOR, m as compileCatalog, n as labGate, o as STANDARD_CATALOG_SPEC, p as UNKNOWN, r as labRecordOf, s as STANDARD_FAULTS, t as LAB_RECORD_FORMAT, u as NONE, v as predictive, x as layerRank, y as LAYERS } from "./diagnosis-a4hRzFxp.js";
2
+ import { C as observe, D as parseTarget, E as hostKind, O as sameTarget, S as isFactOf, T as formatTarget, _ as TRACE_OUTCOMES, a as ECHO_OUTCOMES, b as defineFactKind, c as HTTP_RESPONSE, d as PATH_REPORT, f as PATH_TRACE, g as TOOL_KINDS, h as TLS_HANDSHAKE, i as DNS_OUTCOMES, k as target, l as ICMP_ECHO, m as TCP_CONNECT, n as CONNECT_OUTCOMES, o as HANDSHAKE_OUTCOMES, p as RECORD_TYPES, r as DNS_ANSWER, s as HTTP_OUTCOMES, t as CERT_PROBLEMS, u as NATIVE_KINDS, v as TRUSTS, w as canonicalHost, x as factsOf, y as defineProbe } from "./kinds-CQv769lK.js";
3
+ import { ADDRESS_CLASSES, addressClass, addressFamily, addressKind, canonicalAddress, cidrContains, cidrSet, formatCidr, parseCidr } from "./address.js";
4
+ import { connect, isIP } from "node:net";
5
+ import { err, ok } from "@mark1russell7/optional";
6
+ import { Resolver } from "node:dns/promises";
7
+ import { X509Certificate } from "node:crypto";
8
+ import { request } from "node:http";
9
+ import * as tls from "node:tls";
10
+ // src/scope.ts
11
+ /** The default limit of the different ports of one host in one runner. */
12
+ const DEFAULT_MAX_PORTS_PER_HOST = 8;
13
+ /** This function gives the problems of a grant, or an empty list for a correct grant. */
14
+ function grantProblems(grant) {
15
+ if (grant.kind === "range") {
16
+ const range = parseCidr(grant.cidr);
17
+ return range.tag === "err" ? [range.error] : [];
18
+ }
19
+ const host = canonicalHost(grant.host);
20
+ if (host.length === 0) return ["A host grant has an empty host."];
21
+ if (isIP(host) !== 0) return [`The host grant "${grant.host}" is an IP address. Use a range grant for an address.`];
22
+ return [];
23
+ }
24
+ /**
25
+ * This function makes a scope from grants. It refuses a grant with a problem, so a typing error cannot widen the scope.
26
+ *
27
+ * The default limit of ports is `DEFAULT_MAX_PORTS_PER_HOST`.
28
+ */
29
+ function scopeOf(grants, options = {}) {
30
+ const problems = grants.flatMap(grantProblems);
31
+ const maxPortsPerHost = options.maxPortsPerHost ?? 8;
32
+ if (!Number.isInteger(maxPortsPerHost) || maxPortsPerHost < 1) problems.push("The limit of ports for each host is not a positive integer.");
33
+ if (problems.length > 0) return err(problems);
34
+ return ok({
35
+ grants: [...grants],
36
+ maxPortsPerHost
37
+ });
38
+ }
39
+ /**
40
+ * This function gives the grant for a target that the user names. A name gets a host grant. An IP address gets a
41
+ * range grant of one address.
42
+ */
43
+ function namedGrant(t, note) {
44
+ if (t.kind === "name") return {
45
+ kind: "host",
46
+ host: t.host,
47
+ basis: "named",
48
+ note
49
+ };
50
+ return {
51
+ kind: "range",
52
+ cidr: `${t.host}/${t.kind === "ipv4" ? 32 : 128}`,
53
+ basis: "named",
54
+ note
55
+ };
56
+ }
57
+ /** This function gives the grant of one address that a DNS answer for a granted name gave. */
58
+ function resolvedGrant(name, address, factId) {
59
+ const host = canonicalHost(address);
60
+ const family = isIP(host);
61
+ if (family === 0) throw new RangeError(`"${address}" is not an IP address`);
62
+ const note = factId === void 0 ? `${canonicalHost(name)} resolved to it` : `${canonicalHost(name)} resolved to it (fact ${factId})`;
63
+ return {
64
+ kind: "range",
65
+ cidr: `${host}/${family === 4 ? 32 : 128}`,
66
+ basis: "resolved",
67
+ note
68
+ };
69
+ }
70
+ /** This function gives a scope with more grants. The limit of ports does not change. */
71
+ function widen(scope, grants) {
72
+ return scopeOf([...scope.grants, ...grants], { maxPortsPerHost: scope.maxPortsPerHost });
73
+ }
74
+ function grantFor(scope, peer) {
75
+ const host = canonicalHost(peer.host);
76
+ const family = isIP(host);
77
+ for (const grant of scope.grants) {
78
+ if (grant.kind === "host") {
79
+ if (family !== 0) continue;
80
+ const granted = canonicalHost(grant.host);
81
+ if (host === granted || grant.subdomains === true && host.endsWith(`.${granted}`)) return grant;
82
+ continue;
83
+ }
84
+ if (family === 0) continue;
85
+ const range = parseCidr(grant.cidr);
86
+ if (range.tag === "ok" && cidrContains(range.value, host)) return grant;
87
+ }
88
+ }
89
+ /**
90
+ * This function decides if an active probe can send packets to a peer.
91
+ *
92
+ * The peer must match a grant of the scope. A new port of a host must also stay in the limit of ports for each host.
93
+ * The ledger has the ports that the runner sent packets to before.
94
+ */
95
+ function decideScope(scope, peer, ledger = /* @__PURE__ */ new Map()) {
96
+ const grant = grantFor(scope, peer);
97
+ if (grant === void 0) return {
98
+ allowed: false,
99
+ reason: `${formatTarget(peer)} is not in the scope. Name it, or add a grant for a network that you own.`
100
+ };
101
+ if (peer.port !== void 0) {
102
+ const ports = ledger.get(canonicalHost(peer.host)) ?? /* @__PURE__ */ new Set();
103
+ if (!ports.has(peer.port) && ports.size >= scope.maxPortsPerHost) return {
104
+ allowed: false,
105
+ reason: `${formatTarget(peer)} is a new port after ${ports.size} ports of the same host. netdiag does no port scan.`
106
+ };
107
+ }
108
+ return {
109
+ allowed: true,
110
+ grant
111
+ };
112
+ }
113
+ // src/run.ts
114
+ function message(thrown) {
115
+ if (thrown instanceof Error) return thrown.message;
116
+ return String(thrown);
117
+ }
118
+ function abortReason(signal) {
119
+ const reason = signal.reason;
120
+ return reason === void 0 ? "The signal stopped the run." : message(reason);
121
+ }
122
+ /** This function makes a runner. */
123
+ function createRunner(options) {
124
+ const { clock } = options;
125
+ const origin = {
126
+ wall: options.wall ?? (/* @__PURE__ */ new Date()).toISOString(),
127
+ monotonic: clock.now()
128
+ };
129
+ let scope = options.scope;
130
+ const facts = [];
131
+ let ledger = /* @__PURE__ */ new Map();
132
+ let next = 1;
133
+ /** This function gives the ledger with the ports of the peers, or the refusal of the first peer out of the scope. */
134
+ function reserve(peers) {
135
+ const planned = new Map([...ledger].map(([host, ports]) => [host, new Set(ports)]));
136
+ for (const peer of peers) {
137
+ const decision = decideScope(scope, peer, planned);
138
+ if (!decision.allowed) return {
139
+ peer,
140
+ reason: decision.reason
141
+ };
142
+ if (peer.port === void 0) continue;
143
+ const host = canonicalHost(peer.host);
144
+ planned.set(host, (planned.get(host) ?? /* @__PURE__ */ new Set()).add(peer.port));
145
+ }
146
+ return planned;
147
+ }
148
+ function stamp(probe, observations, time) {
149
+ const stamped = [];
150
+ for (const observation of observations) {
151
+ const { kind } = observation;
152
+ if (!probe.kinds.some((known) => known.kind === kind.kind)) return `The probe gave the kind "${kind.kind}", which it does not declare.`;
153
+ if (!kind.check(observation.value)) return `The value of a "${kind.kind}" fact does not pass the check of its kind.`;
154
+ stamped.push({
155
+ id: `f${next + stamped.length}`,
156
+ kind: kind.kind,
157
+ layer: kind.layer,
158
+ subject: observation.subject,
159
+ time: observation.time ?? time,
160
+ source: probe.tool === void 0 ? {
161
+ probe: probe.id,
162
+ version: probe.version
163
+ } : {
164
+ probe: probe.id,
165
+ version: probe.version,
166
+ tool: probe.tool
167
+ },
168
+ value: observation.value
169
+ });
170
+ }
171
+ return stamped;
172
+ }
173
+ async function run(probe, input, signal = new AbortController().signal) {
174
+ if (signal.aborted) return {
175
+ status: "aborted",
176
+ reason: abortReason(signal)
177
+ };
178
+ let peers;
179
+ try {
180
+ peers = probe.peers(input);
181
+ } catch (thrown) {
182
+ return {
183
+ status: "failed",
184
+ stage: "probe",
185
+ error: message(thrown)
186
+ };
187
+ }
188
+ if (probe.mode === "passive" && peers.length > 0) return {
189
+ status: "failed",
190
+ stage: "probe",
191
+ error: `The passive probe "${probe.id}" gives peers: ${peers.map(formatTarget).join(", ")}.`
192
+ };
193
+ if (probe.mode === "active" && peers.length === 0) return {
194
+ status: "failed",
195
+ stage: "probe",
196
+ error: `The active probe "${probe.id}" gives no peer.`
197
+ };
198
+ const reserved = reserve(peers);
199
+ if ("reason" in reserved) return {
200
+ status: "refused",
201
+ peer: reserved.peer,
202
+ reason: reserved.reason
203
+ };
204
+ ledger = reserved;
205
+ const context = {
206
+ clock,
207
+ signal
208
+ };
209
+ let availability;
210
+ try {
211
+ availability = await probe.available(context);
212
+ } catch (thrown) {
213
+ return {
214
+ status: "failed",
215
+ stage: "probe",
216
+ error: message(thrown)
217
+ };
218
+ }
219
+ if (!availability.available) return {
220
+ status: "unavailable",
221
+ reason: availability.reason
222
+ };
223
+ if (signal.aborted) return {
224
+ status: "aborted",
225
+ reason: abortReason(signal)
226
+ };
227
+ const start = clock.now();
228
+ let raw;
229
+ try {
230
+ raw = await probe.run(input, context);
231
+ } catch (thrown) {
232
+ if (signal.aborted) return {
233
+ status: "aborted",
234
+ reason: abortReason(signal)
235
+ };
236
+ return {
237
+ status: "failed",
238
+ stage: "run",
239
+ error: message(thrown)
240
+ };
241
+ }
242
+ const time = {
243
+ start,
244
+ end: clock.now()
245
+ };
246
+ let observations;
247
+ try {
248
+ observations = probe.parse(raw, input);
249
+ } catch (thrown) {
250
+ return {
251
+ status: "failed",
252
+ stage: "parse",
253
+ error: message(thrown),
254
+ raw
255
+ };
256
+ }
257
+ const stamped = stamp(probe, observations, time);
258
+ if (typeof stamped === "string") return {
259
+ status: "failed",
260
+ stage: "parse",
261
+ error: stamped,
262
+ raw
263
+ };
264
+ facts.push(...stamped);
265
+ next += stamped.length;
266
+ return {
267
+ status: "done",
268
+ time,
269
+ raw,
270
+ facts: stamped
271
+ };
272
+ }
273
+ return {
274
+ origin,
275
+ scope: () => scope,
276
+ facts: () => [...facts],
277
+ grant(grants) {
278
+ const widened = widen(scope, grants);
279
+ if (widened.tag === "err") throw new RangeError(widened.error.join(" "));
280
+ scope = widened.value;
281
+ },
282
+ run
283
+ };
284
+ }
285
+ // src/probes/socket.ts
286
+ /** The time limit of a probe run, when its input gives none: 5 seconds. */
287
+ const DEFAULT_TIMEOUT_MS = 5e3;
288
+ const defaultDial = (options) => connect(options);
289
+ /** This function gives the dial of the options. */
290
+ function dialOf(options) {
291
+ return options.dial ?? defaultDial;
292
+ }
293
+ /** This function gives the error code of a thrown value, for example `ECONNREFUSED`. */
294
+ function codeOf(thrown) {
295
+ if (typeof thrown !== "object" || thrown === null) return void 0;
296
+ const code = thrown.code;
297
+ return typeof code === "string" && code.length > 0 ? code : void 0;
298
+ }
299
+ function syscallOf(thrown) {
300
+ if (typeof thrown !== "object" || thrown === null) return void 0;
301
+ const syscall = thrown.syscall;
302
+ return typeof syscall === "string" ? syscall : void 0;
303
+ }
304
+ /** This function gives the peer of a probe that connects. It throws for an address that is not an IP address. */
305
+ function peerOf(address, port) {
306
+ if (isIP(address) === 0) throw new TypeError(`"${address}" is not an IP address. Get the addresses of a name with the DNS probe first.`);
307
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new RangeError(`${port} is not a port from 1 to 65535.`);
308
+ return target(address, port);
309
+ }
310
+ /** This function gives the time limit of an input. It throws for a limit that is not a positive number. */
311
+ function timeoutOf(timeoutMs) {
312
+ const limit = timeoutMs ?? 5e3;
313
+ if (!Number.isFinite(limit) || limit <= 0) throw new RangeError(`The time limit ${limit} is not a positive number of milliseconds.`);
314
+ return limit;
315
+ }
316
+ /** This function starts the watch of a probe run. */
317
+ function watch(context, timeoutMs) {
318
+ let expired = false;
319
+ let current;
320
+ const stop = () => {
321
+ current?.destroy();
322
+ };
323
+ const cancel = context.clock.after(timeoutMs, () => {
324
+ expired = true;
325
+ stop();
326
+ });
327
+ context.signal.addEventListener("abort", stop, { once: true });
328
+ return {
329
+ expired: () => expired,
330
+ guard(socket) {
331
+ current = socket;
332
+ if (expired || context.signal.aborted) socket.destroy();
333
+ },
334
+ close() {
335
+ cancel();
336
+ context.signal.removeEventListener("abort", stop);
337
+ }
338
+ };
339
+ }
340
+ const ignore = () => void 0;
341
+ /** This function maps an error code of a TCP connect to its outcome. */
342
+ function connectOutcome(code) {
343
+ switch (code) {
344
+ case "ECONNREFUSED": return "refused";
345
+ case "ETIMEDOUT": return "timeout";
346
+ case "EHOSTUNREACH":
347
+ case "ENETUNREACH":
348
+ case "EHOSTDOWN":
349
+ case "ENETDOWN": return "unreachable";
350
+ case "ECONNRESET": return "reset";
351
+ default: return "error";
352
+ }
353
+ }
354
+ /**
355
+ * This function tells how a stream ended before its phase was complete.
356
+ *
357
+ * Node.js gives the code `ECONNRESET` for a TCP reset, with the system call that read it. It gives the same code with
358
+ * no system call when the peer closed the connection with a FIN.
359
+ */
360
+ function endOf(thrown, expired) {
361
+ if (expired) return "timeout";
362
+ const code = codeOf(thrown);
363
+ if (code === "EPIPE") return "reset";
364
+ if (code !== "ECONNRESET") return void 0;
365
+ return syscallOf(thrown) === void 0 ? "closed" : "reset";
366
+ }
367
+ const NOT_TLS = /* @__PURE__ */ new Set([
368
+ "ERR_SSL_WRONG_VERSION_NUMBER",
369
+ "ERR_SSL_PACKET_LENGTH_TOO_LONG",
370
+ "ERR_SSL_HTTP_REQUEST",
371
+ "ERR_SSL_RECORD_LAYER_FAILURE"
372
+ ]);
373
+ const MISMATCH = /* @__PURE__ */ new Set([
374
+ "ERR_SSL_UNSUPPORTED_PROTOCOL",
375
+ "ERR_SSL_NO_PROTOCOLS_AVAILABLE",
376
+ "ERR_SSL_NO_SHARED_CIPHER",
377
+ "ERR_SSL_WRONG_SSL_VERSION"
378
+ ]);
379
+ /** This function maps an error of a TLS handshake to its outcome. */
380
+ function handshakeOutcome(thrown, expired) {
381
+ const end = endOf(thrown, expired);
382
+ if (end !== void 0) return end;
383
+ const code = codeOf(thrown) ?? "";
384
+ if (NOT_TLS.has(code)) return "not-tls";
385
+ if (code.includes("_ALERT_") || MISMATCH.has(code)) return "alert";
386
+ return "error";
387
+ }
388
+ /** This function maps the authorization error of a TLS socket to the problem of the certificate. */
389
+ function certProblem(code) {
390
+ switch (code) {
391
+ case "CERT_HAS_EXPIRED": return "expired";
392
+ case "CERT_NOT_YET_VALID": return "not-yet-valid";
393
+ case "ERR_TLS_CERT_ALTNAME_INVALID":
394
+ case "HOSTNAME_MISMATCH": return "name";
395
+ case "DEPTH_ZERO_SELF_SIGNED_CERT":
396
+ case "SELF_SIGNED_CERT_IN_CHAIN":
397
+ case "UNABLE_TO_GET_ISSUER_CERT":
398
+ case "UNABLE_TO_GET_ISSUER_CERT_LOCALLY":
399
+ case "UNABLE_TO_VERIFY_LEAF_SIGNATURE":
400
+ case "CERT_UNTRUSTED": return "untrusted";
401
+ case "CERT_REVOKED": return "revoked";
402
+ default: return "other";
403
+ }
404
+ }
405
+ /** This function connects to an address and a port, and gives the socket when it connected. */
406
+ function connectPhase(dial, address, port, w, clock) {
407
+ const family = isIP(address) === 6 ? 6 : 4;
408
+ const start = clock.now();
409
+ return new Promise((resolve) => {
410
+ const socket = dial({
411
+ host: address,
412
+ port,
413
+ family
414
+ });
415
+ socket.on("error", ignore);
416
+ let done = false;
417
+ const finish = (outcome, code) => {
418
+ if (done) return;
419
+ done = true;
420
+ const phase = {
421
+ address,
422
+ port,
423
+ family,
424
+ start,
425
+ end: clock.now(),
426
+ outcome,
427
+ ...code === void 0 ? {} : { code }
428
+ };
429
+ if (outcome === "connected") resolve({
430
+ socket,
431
+ phase
432
+ });
433
+ else {
434
+ socket.destroy();
435
+ resolve({ phase });
436
+ }
437
+ };
438
+ socket.once("connect", () => finish("connected"));
439
+ socket.once("error", (error) => w.expired() ? finish("timeout") : finish(connectOutcome(codeOf(error)), codeOf(error)));
440
+ socket.once("close", () => w.expired() ? finish("timeout") : finish("error", "ECLOSED"));
441
+ w.guard(socket);
442
+ });
443
+ }
444
+ /** This function gives the observation of a TCP phase. */
445
+ function tcpObservation(phase) {
446
+ const { start, end, ...rest } = phase;
447
+ return observe(TCP_CONNECT, target(phase.address, phase.port), {
448
+ ...rest,
449
+ ms: end - start
450
+ }, {
451
+ start,
452
+ end
453
+ });
454
+ }
455
+ function systemRoots() {
456
+ const get = tls.getCACertificates;
457
+ if (typeof get !== "function") return [];
458
+ try {
459
+ const roots = get.call(tls, "system");
460
+ return Array.isArray(roots) ? roots.filter((root) => typeof root === "string") : [];
461
+ } catch {
462
+ return [];
463
+ }
464
+ }
465
+ /**
466
+ * This function makes the store of trust of a TLS probe. It has the public roots of Node.js, the roots of the computer
467
+ * and the roots that the user gives. A chain that ends at a public root has the trust `public`. A chain that ends at a
468
+ * different root of the store has the trust `private`, for example the root of a proxy that examines TLS. The roots
469
+ * in `extraPublic` count as public roots.
470
+ */
471
+ function trustStore(extraRoots = [], extraPublic = []) {
472
+ const local = [...systemRoots(), ...extraRoots];
473
+ const publicRoots = new Set([...tls.rootCertificates, ...extraPublic].flatMap(fingerprintOf));
474
+ const privateRoots = new Set(local.flatMap(fingerprintOf).filter((fingerprint) => !publicRoots.has(fingerprint)));
475
+ return {
476
+ context: tls.createSecureContext({ ca: [
477
+ ...tls.rootCertificates,
478
+ ...extraPublic,
479
+ ...local
480
+ ] }),
481
+ publicRoots,
482
+ privateRoots
483
+ };
484
+ }
485
+ function fingerprintOf(pem) {
486
+ try {
487
+ return [new X509Certificate(pem).fingerprint256];
488
+ } catch {
489
+ return [];
490
+ }
491
+ }
492
+ function text(value) {
493
+ if (value === void 0) return "";
494
+ return Array.isArray(value) ? value.join(", ") : value;
495
+ }
496
+ function isoTime(value) {
497
+ const time = new Date(value);
498
+ return Number.isNaN(time.getTime()) ? value : time.toISOString();
499
+ }
500
+ function namesOf(subjectAltName) {
501
+ if (subjectAltName === void 0 || subjectAltName.length === 0) return [];
502
+ return subjectAltName.split(", ").map((entry) => entry.replace(/^(?:DNS|IP Address):/, "")).filter((entry) => entry.length > 0);
503
+ }
504
+ function isSelfSigned(raw) {
505
+ try {
506
+ const certificate = new X509Certificate(raw);
507
+ return certificate.issuer === certificate.subject && certificate.verify(certificate.publicKey);
508
+ } catch {
509
+ return false;
510
+ }
511
+ }
512
+ function certificateOf(socket, store) {
513
+ const leaf = socket.getPeerCertificate(true);
514
+ if (Object.keys(leaf).length === 0) return void 0;
515
+ let top = leaf;
516
+ let chain = 1;
517
+ const seen = /* @__PURE__ */ new Set([leaf.fingerprint256]);
518
+ while (top.issuerCertificate !== void 0 && !seen.has(top.issuerCertificate.fingerprint256)) {
519
+ top = top.issuerCertificate;
520
+ seen.add(top.fingerprint256);
521
+ chain += 1;
522
+ }
523
+ const authorized = socket.authorized;
524
+ const trust = store.publicRoots.has(top.fingerprint256) ? "public" : store.privateRoots.has(top.fingerprint256) || authorized ? "private" : "none";
525
+ const reason = socket.authorizationError;
526
+ return {
527
+ subject: text(leaf.subject.CN) || text(leaf.subject.O),
528
+ issuer: text(leaf.issuer.CN) || text(leaf.issuer.O),
529
+ names: namesOf(leaf.subjectaltname),
530
+ validFrom: isoTime(leaf.valid_from),
531
+ validTo: isoTime(leaf.valid_to),
532
+ fingerprint256: leaf.fingerprint256,
533
+ selfSigned: isSelfSigned(leaf.raw),
534
+ chain,
535
+ trust,
536
+ ...authorized ? {} : { problem: certProblem(reason instanceof Error ? codeOf(reason) ?? reason.message : String(reason)) }
537
+ };
538
+ }
539
+ /**
540
+ * This function does a TLS handshake on a connected socket. It does not refuse a certificate that is not valid: it
541
+ * gives the outcome `cert-error` and the problem of the certificate.
542
+ */
543
+ function handshakePhase(socket, input, store, w, clock) {
544
+ const start = clock.now();
545
+ const servername = input.servername !== void 0 && isIP(input.servername) === 0 ? input.servername : void 0;
546
+ return new Promise((resolve) => {
547
+ const secure = tls.connect({
548
+ socket,
549
+ host: input.address,
550
+ ...servername === void 0 ? {} : { servername },
551
+ ...input.alpn === void 0 ? {} : { ALPNProtocols: [...input.alpn] },
552
+ secureContext: store.context,
553
+ rejectUnauthorized: false
554
+ });
555
+ secure.on("error", ignore);
556
+ let done = false;
557
+ const base = {
558
+ ...servername === void 0 ? {} : { servername },
559
+ start
560
+ };
561
+ const finish = (phase, ok) => {
562
+ if (done) return;
563
+ done = true;
564
+ if (ok) resolve({
565
+ secure,
566
+ phase
567
+ });
568
+ else {
569
+ secure.destroy();
570
+ resolve({ phase });
571
+ }
572
+ };
573
+ secure.once("secureConnect", () => {
574
+ const cipher = secure.getCipher().name;
575
+ const protocol = secure.getProtocol() ?? void 0;
576
+ const alpn = typeof secure.alpnProtocol === "string" ? secure.alpnProtocol : void 0;
577
+ const certificate = certificateOf(secure, store);
578
+ finish({
579
+ ...base,
580
+ end: clock.now(),
581
+ outcome: secure.authorized ? "ok" : "cert-error",
582
+ ...protocol === void 0 ? {} : { protocol },
583
+ ...cipher.length === 0 ? {} : { cipher },
584
+ ...alpn === void 0 ? {} : { alpn },
585
+ ...certificate === void 0 ? {} : { certificate }
586
+ }, true);
587
+ });
588
+ secure.once("error", (error) => {
589
+ const code = codeOf(error);
590
+ finish({
591
+ ...base,
592
+ end: clock.now(),
593
+ outcome: handshakeOutcome(error, w.expired()),
594
+ ...code === void 0 ? {} : { code }
595
+ }, false);
596
+ });
597
+ secure.once("close", () => finish({
598
+ ...base,
599
+ end: clock.now(),
600
+ outcome: w.expired() ? "timeout" : "closed"
601
+ }, false));
602
+ w.guard(secure);
603
+ });
604
+ }
605
+ /** This function gives the observation of a TLS phase. */
606
+ function tlsObservation(address, port, phase) {
607
+ const { start, end, ...rest } = phase;
608
+ return observe(TLS_HANDSHAKE, target(address, port), {
609
+ address,
610
+ port,
611
+ ...rest,
612
+ ms: end - start
613
+ }, {
614
+ start,
615
+ end
616
+ });
617
+ }
618
+ /** The fields of a response head that an HTTP fact keeps. */
619
+ const KEPT_FIELDS = [
620
+ "server",
621
+ "via",
622
+ "location",
623
+ "content-type",
624
+ "content-length",
625
+ "alt-svc",
626
+ "x-cache",
627
+ "cf-ray"
628
+ ];
629
+ function httpOutcome(thrown, expired) {
630
+ const end = endOf(thrown, expired);
631
+ if (end !== void 0) return end;
632
+ return (codeOf(thrown) ?? "").startsWith("HPE_") ? "protocol" : "error";
633
+ }
634
+ /** This function sends one HTTP/1.1 request on a connected socket, and reads the response up to a limit of bytes. */
635
+ function exchangePhase(socket, input, w, clock) {
636
+ return new Promise((resolve) => {
637
+ let done = false;
638
+ let head;
639
+ let status;
640
+ let version;
641
+ let fields = {};
642
+ let bytes = 0;
643
+ let truncated = false;
644
+ const start = clock.now();
645
+ const finish = (outcome, code) => {
646
+ if (done) return;
647
+ done = true;
648
+ resolve({
649
+ method: input.method,
650
+ host: input.host,
651
+ path: input.path,
652
+ start,
653
+ ...head === void 0 ? {} : { head },
654
+ end: clock.now(),
655
+ outcome,
656
+ ...status === void 0 ? {} : { status },
657
+ ...version === void 0 ? {} : { version },
658
+ fields,
659
+ bytes,
660
+ truncated,
661
+ ...code === void 0 ? {} : { code }
662
+ });
663
+ socket.destroy();
664
+ };
665
+ const req = request({
666
+ createConnection: () => socket,
667
+ method: input.method,
668
+ path: input.path,
669
+ headers: {
670
+ host: input.host,
671
+ "user-agent": "netdiag",
672
+ accept: "*/*",
673
+ connection: "close"
674
+ }
675
+ });
676
+ req.on("error", (error) => finish(httpOutcome(error, w.expired()), codeOf(error)));
677
+ req.on("close", () => finish(w.expired() ? "timeout" : "closed"));
678
+ req.on("response", (res) => {
679
+ head = clock.now();
680
+ status = res.statusCode;
681
+ version = res.httpVersion;
682
+ fields = Object.fromEntries(KEPT_FIELDS.flatMap((name) => {
683
+ const value = res.headers[name];
684
+ return value === void 0 ? [] : [[name, Array.isArray(value) ? value.join(", ") : value]];
685
+ }));
686
+ res.on("data", (chunk) => {
687
+ bytes += chunk.length;
688
+ if (bytes >= input.maxBytes) {
689
+ truncated = true;
690
+ finish("response");
691
+ }
692
+ });
693
+ res.on("end", () => finish("response"));
694
+ res.on("error", (error) => finish(httpOutcome(error, w.expired()), codeOf(error)));
695
+ });
696
+ req.end();
697
+ });
698
+ }
699
+ /** This function gives the observation of an HTTP phase. */
700
+ function httpObservation(address, port, scheme, phase) {
701
+ const { start, end, head, ...rest } = phase;
702
+ const time = {
703
+ start,
704
+ end
705
+ };
706
+ return observe(HTTP_RESPONSE, target(address, port), {
707
+ address,
708
+ port,
709
+ scheme,
710
+ ...rest,
711
+ ...head === void 0 ? {} : { ttfbMs: head - start },
712
+ ms: end - start
713
+ }, time);
714
+ }
715
+ // src/probes/native.ts
716
+ const NATIVE = {
717
+ available: true,
718
+ version: process.versions.node
719
+ };
720
+ function native() {
721
+ return Promise.resolve(NATIVE);
722
+ }
723
+ /** This function maps an error code of a DNS query to its outcome. */
724
+ function dnsOutcome(code) {
725
+ switch (code) {
726
+ case "ENOTFOUND": return "nxdomain";
727
+ case "ENODATA": return "nodata";
728
+ case "ESERVFAIL": return "servfail";
729
+ case "EREFUSED": return "refused";
730
+ case "ETIMEOUT": return "timeout";
731
+ case "ECONNREFUSED": return "unreachable";
732
+ default: return "error";
733
+ }
734
+ }
735
+ /**
736
+ * The DNS probe sends one query for the addresses of a name to one resolver. Its peer is the resolver, not the name.
737
+ * The query uses no search domain.
738
+ */
739
+ const DNS_PROBE = defineProbe({
740
+ id: "dns.query",
741
+ version: "1",
742
+ summary: "One DNS query for the A or AAAA records of a name, to one resolver.",
743
+ layer: "dns",
744
+ mode: "active",
745
+ kinds: [DNS_ANSWER],
746
+ peers(input) {
747
+ if (isIP(input.name) !== 0) throw new TypeError(`"${input.name}" is an address, not a name.`);
748
+ return [peerOf(input.resolver, input.port ?? 53)];
749
+ },
750
+ available: native,
751
+ async run(input, context) {
752
+ const record = input.record ?? "A";
753
+ const limit = timeoutOf(input.timeoutMs);
754
+ const server = formatTarget(target(input.resolver, input.port ?? 53));
755
+ const resolver = new Resolver({
756
+ timeout: limit,
757
+ tries: 1
758
+ });
759
+ resolver.setServers([server]);
760
+ let expired = false;
761
+ const stop = () => resolver.cancel();
762
+ const cancel = context.clock.after(limit, () => {
763
+ expired = true;
764
+ stop();
765
+ });
766
+ context.signal.addEventListener("abort", stop, { once: true });
767
+ const start = context.clock.now();
768
+ const base = {
769
+ name: input.name,
770
+ record,
771
+ resolver: server,
772
+ start
773
+ };
774
+ try {
775
+ const answers = record === "A" ? await resolver.resolve4(input.name, { ttl: true }) : await resolver.resolve6(input.name, { ttl: true });
776
+ const raw = {
777
+ ...base,
778
+ end: context.clock.now(),
779
+ outcome: "ok",
780
+ addresses: answers.map((answer) => answer.address),
781
+ ...answers.length === 0 ? {} : { ttl: Math.min(...answers.map((answer) => answer.ttl)) }
782
+ };
783
+ context.signal.throwIfAborted();
784
+ return raw;
785
+ } catch (thrown) {
786
+ context.signal.throwIfAborted();
787
+ const code = codeOf(thrown);
788
+ return {
789
+ ...base,
790
+ end: context.clock.now(),
791
+ outcome: expired ? "timeout" : dnsOutcome(code),
792
+ addresses: [],
793
+ ...code === void 0 ? {} : { code }
794
+ };
795
+ } finally {
796
+ cancel();
797
+ context.signal.removeEventListener("abort", stop);
798
+ }
799
+ },
800
+ parse(raw) {
801
+ const { start, end, ...rest } = raw;
802
+ return [observe(DNS_ANSWER, target(raw.name), {
803
+ ...rest,
804
+ ms: end - start
805
+ }, {
806
+ start,
807
+ end
808
+ })];
809
+ }
810
+ });
811
+ /** This function makes the TCP probe. It connects, and then it closes the connection. */
812
+ function tcpProbe(options = {}) {
813
+ const dial = dialOf(options);
814
+ return defineProbe({
815
+ id: "tcp.connect",
816
+ version: "1",
817
+ summary: "One TCP connect to an IP address and a port.",
818
+ layer: "tcp",
819
+ mode: "active",
820
+ kinds: [TCP_CONNECT],
821
+ peers: (input) => [peerOf(input.address, input.port)],
822
+ available: native,
823
+ async run(input, context) {
824
+ const w = watch(context, timeoutOf(input.timeoutMs));
825
+ try {
826
+ const { socket, phase } = await connectPhase(dial, input.address, input.port, w, context.clock);
827
+ socket?.destroy();
828
+ context.signal.throwIfAborted();
829
+ return { tcp: phase };
830
+ } finally {
831
+ w.close();
832
+ }
833
+ },
834
+ parse: (raw) => [tcpObservation(raw.tcp)]
835
+ });
836
+ }
837
+ /** The TCP probe with the `connect` of `node:net`. */
838
+ const TCP_PROBE = tcpProbe();
839
+ function lazyStore(options) {
840
+ let store;
841
+ return () => store ??= trustStore(options.roots ?? [], options.publicRoots ?? []);
842
+ }
843
+ /**
844
+ * This function makes the TLS probe. It measures the TCP connect and the TLS handshake apart. It examines the
845
+ * certificate, and it does not refuse a certificate that is not valid.
846
+ */
847
+ function tlsProbe(options = {}) {
848
+ const dial = dialOf(options);
849
+ const store = lazyStore(options);
850
+ return defineProbe({
851
+ id: "tls.handshake",
852
+ version: "1",
853
+ summary: "One TCP connect and one TLS handshake, with the certificate of the server.",
854
+ layer: "tls",
855
+ mode: "active",
856
+ kinds: [TCP_CONNECT, TLS_HANDSHAKE],
857
+ peers: (input) => [peerOf(input.address, input.port ?? 443)],
858
+ available: native,
859
+ async run(input, context) {
860
+ const port = input.port ?? 443;
861
+ const w = watch(context, timeoutOf(input.timeoutMs));
862
+ try {
863
+ const { socket, phase } = await connectPhase(dial, input.address, port, w, context.clock);
864
+ if (socket === void 0) {
865
+ context.signal.throwIfAborted();
866
+ return { tcp: phase };
867
+ }
868
+ const handshake = await handshakePhase(socket, {
869
+ address: input.address,
870
+ servername: input.servername,
871
+ alpn: input.alpn
872
+ }, store(), w, context.clock);
873
+ handshake.secure?.destroy();
874
+ context.signal.throwIfAborted();
875
+ return {
876
+ tcp: phase,
877
+ tls: handshake.phase
878
+ };
879
+ } finally {
880
+ w.close();
881
+ }
882
+ },
883
+ parse(raw) {
884
+ const observations = [tcpObservation(raw.tcp)];
885
+ if (raw.tls !== void 0) observations.push(tlsObservation(raw.tcp.address, raw.tcp.port, raw.tls));
886
+ return observations;
887
+ }
888
+ });
889
+ }
890
+ /** The TLS probe with the public roots and the roots of the computer. */
891
+ const TLS_PROBE = tlsProbe();
892
+ /** The limit of the bytes of a body that the HTTP probe reads, when its input gives none: 64 KiB. */
893
+ const DEFAULT_MAX_BYTES = 65536;
894
+ function hostField(input, port) {
895
+ const host = input.host ?? (isIP(input.address) === 6 ? `[${input.address}]` : input.address);
896
+ return port === (input.scheme === "https" ? 443 : 80) ? host : `${host}:${port}`;
897
+ }
898
+ /**
899
+ * This function makes the HTTP probe. It sends one request, follows no redirect, and reads the body up to a limit. It
900
+ * gives the facts of each phase: TCP, TLS for `https`, and HTTP. For `https`, it sends the request only when the
901
+ * certificate passes its check.
902
+ */
903
+ function httpProbe(options = {}) {
904
+ const dial = dialOf(options);
905
+ const store = lazyStore(options);
906
+ const portOf = (input) => input.port ?? (input.scheme === "https" ? 443 : 80);
907
+ return defineProbe({
908
+ id: "http.request",
909
+ version: "1",
910
+ summary: "One HTTP/1.1 request, with the time of each phase: TCP, TLS and the first byte.",
911
+ layer: "http",
912
+ mode: "active",
913
+ kinds: [
914
+ TCP_CONNECT,
915
+ TLS_HANDSHAKE,
916
+ HTTP_RESPONSE
917
+ ],
918
+ peers(input) {
919
+ const path = input.path ?? "/";
920
+ if (!path.startsWith("/") || /[\s]/.test(path)) throw new TypeError(`"${path}" is not a path that starts with "/" and has no space.`);
921
+ return [peerOf(input.address, portOf(input))];
922
+ },
923
+ available: native,
924
+ async run(input, context) {
925
+ const port = portOf(input);
926
+ const maxBytes = input.maxBytes ?? 65536;
927
+ if (!Number.isInteger(maxBytes) || maxBytes < 1) throw new RangeError(`The limit of bytes ${maxBytes} is not a positive integer.`);
928
+ const w = watch(context, timeoutOf(input.timeoutMs));
929
+ try {
930
+ const { socket, phase: tcp } = await connectPhase(dial, input.address, port, w, context.clock);
931
+ if (socket === void 0) {
932
+ context.signal.throwIfAborted();
933
+ return {
934
+ scheme: input.scheme,
935
+ tcp
936
+ };
937
+ }
938
+ let stream = socket;
939
+ let secured;
940
+ if (input.scheme === "https") {
941
+ const handshake = await handshakePhase(socket, {
942
+ address: input.address,
943
+ servername: input.host,
944
+ alpn: ["http/1.1"]
945
+ }, store(), w, context.clock);
946
+ secured = handshake.phase;
947
+ if (handshake.secure === void 0 || secured.outcome !== "ok") {
948
+ handshake.secure?.destroy();
949
+ context.signal.throwIfAborted();
950
+ return {
951
+ scheme: input.scheme,
952
+ tcp,
953
+ tls: secured
954
+ };
955
+ }
956
+ stream = handshake.secure;
957
+ }
958
+ const http = await exchangePhase(stream, {
959
+ method: input.method ?? "GET",
960
+ host: hostField(input, port),
961
+ path: input.path ?? "/",
962
+ maxBytes
963
+ }, w, context.clock);
964
+ context.signal.throwIfAborted();
965
+ return {
966
+ scheme: input.scheme,
967
+ tcp,
968
+ ...secured === void 0 ? {} : { tls: secured },
969
+ http
970
+ };
971
+ } finally {
972
+ w.close();
973
+ }
974
+ },
975
+ parse(raw) {
976
+ const { address, port } = raw.tcp;
977
+ const observations = [tcpObservation(raw.tcp)];
978
+ if (raw.tls !== void 0) observations.push(tlsObservation(address, port, raw.tls));
979
+ if (raw.http !== void 0) observations.push(httpObservation(address, port, raw.scheme, raw.http));
980
+ return observations;
981
+ }
982
+ });
983
+ }
984
+ /** The HTTP probe with the public roots and the roots of the computer. */
985
+ const HTTP_PROBE = httpProbe();
986
+ // src/readers.ts
987
+ /** The bins of the feature `tcp.time`, in milliseconds. A connect of 900 ms or more sent its SYN again. */
988
+ const TCP_TIME_BINS = [
989
+ {
990
+ below: 150,
991
+ value: "short"
992
+ },
993
+ {
994
+ below: 900,
995
+ value: "long"
996
+ },
997
+ { value: "retry" }
998
+ ];
999
+ /** The bins of the feature `tls.ratio`: the time of the TLS handshake divided by the time of the TCP connect. */
1000
+ const TLS_RATIO_BINS = [
1001
+ {
1002
+ below: 3,
1003
+ value: "low"
1004
+ },
1005
+ {
1006
+ below: 8,
1007
+ value: "mid"
1008
+ },
1009
+ { value: "high" }
1010
+ ];
1011
+ /** The bins of the feature `http.ttfb`: the time to the first byte divided by the time of the TCP connect. */
1012
+ const HTTP_TTFB_BINS = [
1013
+ {
1014
+ below: 4,
1015
+ value: "low"
1016
+ },
1017
+ {
1018
+ below: 20,
1019
+ value: "mid"
1020
+ },
1021
+ { value: "high" }
1022
+ ];
1023
+ /**
1024
+ * The smallest time of a TCP connect in a ratio, in milliseconds. On a local link, a ratio with a smaller time measures
1025
+ * only the cost of the cryptography.
1026
+ */
1027
+ const MIN_CONNECT_MS = 1;
1028
+ /** The value of the feature `dns.answer` for each outcome of a DNS query. `nodata` tells that the name exists. */
1029
+ const DNS_ANSWER_VALUES = {
1030
+ ok: "ok",
1031
+ nodata: "ok",
1032
+ nxdomain: "nxdomain",
1033
+ servfail: "servfail",
1034
+ refused: "refused",
1035
+ timeout: "timeout",
1036
+ unreachable: "unreachable"
1037
+ };
1038
+ /** The value of the feature `tcp.connect` for each outcome of a TCP connect. */
1039
+ const TCP_CONNECT_VALUES = {
1040
+ connected: "connected",
1041
+ refused: "refused",
1042
+ timeout: "timeout",
1043
+ unreachable: "unreachable",
1044
+ reset: "reset"
1045
+ };
1046
+ /**
1047
+ * The value of the feature `tls.handshake` for each outcome of a TLS handshake. The catalog has no value for `not-tls`
1048
+ * and `closed` yet. The lab runs (ND-7) tell which faults cause them.
1049
+ */
1050
+ const TLS_HANDSHAKE_VALUES = {
1051
+ ok: "ok",
1052
+ "cert-error": "cert-error",
1053
+ alert: "alert",
1054
+ timeout: "timeout",
1055
+ reset: "reset"
1056
+ };
1057
+ /** The value of the feature `tls.trust` for each trust. A chain that ends at no root of the store has no public root. */
1058
+ const TLS_TRUST_VALUES = {
1059
+ public: "public",
1060
+ private: "private",
1061
+ none: "private"
1062
+ };
1063
+ /** The value of the feature `icmp.reply` for each outcome of the ICMP echo requests to an address. */
1064
+ const ICMP_REPLY_VALUES = {
1065
+ reply: "reply",
1066
+ none: "none",
1067
+ unreachable: "unreachable"
1068
+ };
1069
+ /**
1070
+ * The value of the feature `gateway.reply` for each outcome of the ICMP echo requests to the gateway. When this computer
1071
+ * tells that the gateway is unreachable, the gateway gives no reply.
1072
+ */
1073
+ const GATEWAY_REPLY_VALUES = {
1074
+ reply: "reply",
1075
+ none: "none",
1076
+ unreachable: "none"
1077
+ };
1078
+ /**
1079
+ * The bins of the feature `path.loss`, on the share of the echo requests with a reply. A share of 1 has no loss. A share
1080
+ * of 0.95 or more has a loss of up to 5 %.
1081
+ */
1082
+ const PATH_LOSS_BINS = [
1083
+ {
1084
+ below: .95,
1085
+ value: "high"
1086
+ },
1087
+ {
1088
+ below: 1,
1089
+ value: "some"
1090
+ },
1091
+ { value: "none" }
1092
+ ];
1093
+ /**
1094
+ * The smallest number of echo requests that gives the feature `path.loss`. With 20 requests, one lost request is a loss
1095
+ * of 5 %. With no reply, the feature `icmp.reply` tells the result, not `path.loss`.
1096
+ */
1097
+ const LOSS_MIN_SENT = 20;
1098
+ /** The suffixes of the names of local networks and of the special-use names (RFC 6761, RFC 6762 and RFC 8375). */
1099
+ const LOCAL_SUFFIXES = [
1100
+ "local",
1101
+ "localhost",
1102
+ "test",
1103
+ "invalid",
1104
+ "example",
1105
+ "home.arpa",
1106
+ "internal",
1107
+ "lan",
1108
+ "home",
1109
+ "corp",
1110
+ "intranet",
1111
+ "private"
1112
+ ];
1113
+ /** This function gives the value of the first bin that holds `x`. */
1114
+ function binOf(bins, x) {
1115
+ for (const bin of bins) if (bin.below === void 0 || x < bin.below) return bin.value;
1116
+ throw new RangeError("The last bin has a limit.");
1117
+ }
1118
+ /** This function tells if a name is a local name: a name with one label, or a name with a local suffix. */
1119
+ function isLocalName(name) {
1120
+ const lower = name.toLowerCase().replace(/\.$/, "");
1121
+ if (!lower.includes(".")) return true;
1122
+ return LOCAL_SUFFIXES.some((suffix) => lower === suffix || lower.endsWith(`.${suffix}`));
1123
+ }
1124
+ const READ_KINDS = [
1125
+ ["dns", DNS_ANSWER],
1126
+ ["tcp", TCP_CONNECT],
1127
+ ["tls", TLS_HANDSHAKE],
1128
+ ["http", HTTP_RESPONSE],
1129
+ ["icmp", ICMP_ECHO],
1130
+ ["report", PATH_REPORT],
1131
+ ["trace", PATH_TRACE]
1132
+ ];
1133
+ /**
1134
+ * This function changes facts into evidence for the model.
1135
+ *
1136
+ * - The reader gives one evidence for each feature, subject and role. When a subject has more facts, the last fact decides.
1137
+ * - For `dns.answer`, one answer `ok` of a name and a resolver decides. For `dns.address`, one private address decides.
1138
+ * - The order of the facts is the order of the runner. A ratio uses the last TCP connect before the handshake or the request.
1139
+ * - An ICMP echo fact gives `gateway.reply` for the gateway, and `icmp.reply` for each other address.
1140
+ * - An ICMP echo fact with replies to `LOSS_MIN_SENT` requests or more gives `path.loss`. A report of `mtr` gives it from
1141
+ * the last hop, when the address answered there.
1142
+ * - A trace of `traceroute` gives no evidence. The catalog has no feature for the hops of a path.
1143
+ */
1144
+ function readEvidence(facts, options = {}) {
1145
+ const primaryOf = options.primary ?? (() => true);
1146
+ const slots = /* @__PURE__ */ new Map();
1147
+ const skipped = [];
1148
+ const sorted = {
1149
+ dns: [],
1150
+ tcp: [],
1151
+ tls: [],
1152
+ http: [],
1153
+ icmp: [],
1154
+ report: [],
1155
+ trace: []
1156
+ };
1157
+ for (const fact of facts) {
1158
+ const entry = READ_KINDS.find(([, kind]) => kind.kind === fact.kind);
1159
+ if (entry === void 0) skipped.push({
1160
+ fact: fact.id,
1161
+ reason: `netdiag has no reader for the kind ${fact.kind}.`
1162
+ });
1163
+ else if (!entry[1].check(fact.value)) skipped.push({
1164
+ fact: fact.id,
1165
+ reason: `The value does not pass the check of the kind ${fact.kind}.`
1166
+ });
1167
+ else sorted[entry[0]].push(fact);
1168
+ }
1169
+ /** This function puts a value in the slot of a feature and a group. A `sticky` value stays when a later fact disagrees. */
1170
+ const note = (feature, group, fact, value, sticky) => {
1171
+ const primary = primaryOf(fact);
1172
+ const key = `${feature}\u0000${primary ? 1 : 0}\u0000${group}`;
1173
+ const slot = slots.get(key);
1174
+ if (slot === void 0) slots.set(key, {
1175
+ feature,
1176
+ primary,
1177
+ value,
1178
+ facts: [fact.id]
1179
+ });
1180
+ else {
1181
+ if (slot.value !== sticky) slot.value = value;
1182
+ slot.facts.push(fact.id);
1183
+ }
1184
+ };
1185
+ const skip = (fact, feature, outcome) => {
1186
+ skipped.push({
1187
+ fact: fact.id,
1188
+ reason: `The outcome "${outcome}" of ${fact.kind} has no value of the feature ${feature}.`
1189
+ });
1190
+ };
1191
+ for (const fact of sorted.dns) {
1192
+ const answer = fact.value;
1193
+ const group = `${answer.name.toLowerCase()}\u0000${answer.resolver}`;
1194
+ const value = DNS_ANSWER_VALUES[answer.outcome];
1195
+ if (value === void 0) {
1196
+ skip(fact, "dns.answer", answer.outcome);
1197
+ continue;
1198
+ }
1199
+ note("dns.answer", group, fact, value, "ok");
1200
+ if (answer.addresses.length > 0 && !isLocalName(answer.name)) note("dns.address", group, fact, answer.addresses.map(addressKind).includes("private") ? "private" : "public", "private");
1201
+ }
1202
+ readFamily(sorted, primaryOf, note);
1203
+ const subject = (value) => `${value.address}\u0000${value.port}`;
1204
+ for (const fact of sorted.tcp) {
1205
+ const connect = fact.value;
1206
+ const value = TCP_CONNECT_VALUES[connect.outcome];
1207
+ if (value === void 0) {
1208
+ skip(fact, "tcp.connect", connect.outcome);
1209
+ continue;
1210
+ }
1211
+ note("tcp.connect", subject(connect), fact, value);
1212
+ if (connect.outcome === "connected") note("tcp.time", subject(connect), fact, binOf(TCP_TIME_BINS, connect.ms));
1213
+ }
1214
+ const connectBefore = (fact, value) => {
1215
+ let best;
1216
+ for (const tcp of sorted.tcp) {
1217
+ if (!(tcp.value.address === value.address && tcp.value.port === value.port && primaryOf(tcp) === primaryOf(fact)) || tcp.value.outcome !== "connected" || tcp.time.end > fact.time.start) continue;
1218
+ if (best === void 0 || tcp.time.end >= best.time.end) best = tcp;
1219
+ }
1220
+ return best;
1221
+ };
1222
+ const ratio = (ms, tcp) => ms / Math.max(tcp.value.ms, 1);
1223
+ for (const fact of sorted.tls) {
1224
+ const handshake = fact.value;
1225
+ const group = `${subject(handshake)}\u0000${handshake.servername ?? ""}`;
1226
+ const value = TLS_HANDSHAKE_VALUES[handshake.outcome];
1227
+ if (value === void 0) {
1228
+ skip(fact, "tls.handshake", handshake.outcome);
1229
+ continue;
1230
+ }
1231
+ note("tls.handshake", group, fact, value);
1232
+ if (handshake.certificate !== void 0) note("tls.trust", group, fact, TLS_TRUST_VALUES[handshake.certificate.trust]);
1233
+ const tcp = handshake.outcome === "ok" || handshake.outcome === "cert-error" ? connectBefore(fact, handshake) : void 0;
1234
+ if (tcp !== void 0) note("tls.ratio", group, fact, binOf(TLS_RATIO_BINS, ratio(handshake.ms, tcp)));
1235
+ }
1236
+ for (const fact of sorted.http) {
1237
+ const response = fact.value;
1238
+ const group = [
1239
+ subject(response),
1240
+ response.scheme,
1241
+ response.host,
1242
+ response.method,
1243
+ response.path
1244
+ ].join("\0");
1245
+ const status = statusValue(response);
1246
+ if (status === void 0) {
1247
+ skip(fact, "http.status", response.outcome === "response" ? `response ${response.status ?? ""}`.trim() : response.outcome);
1248
+ continue;
1249
+ }
1250
+ note("http.status", group, fact, status);
1251
+ const tcp = response.ttfbMs === void 0 ? void 0 : connectBefore(fact, response);
1252
+ if (tcp !== void 0 && response.ttfbMs !== void 0) note("http.ttfb", group, fact, binOf(HTTP_TTFB_BINS, ratio(response.ttfbMs, tcp)));
1253
+ }
1254
+ const gateway = options.gateway === void 0 ? void 0 : canonicalAddress(options.gateway);
1255
+ for (const fact of sorted.icmp) {
1256
+ const echo = fact.value;
1257
+ const address = canonicalAddress(echo.address);
1258
+ const isGateway = address === gateway;
1259
+ const feature = isGateway ? "gateway.reply" : "icmp.reply";
1260
+ const value = (isGateway ? GATEWAY_REPLY_VALUES : ICMP_REPLY_VALUES)[echo.outcome];
1261
+ if (value === void 0) {
1262
+ skip(fact, feature, echo.outcome);
1263
+ continue;
1264
+ }
1265
+ note(feature, address, fact, value);
1266
+ }
1267
+ const lossFacts = /* @__PURE__ */ new Set([...sorted.icmp, ...sorted.report]);
1268
+ for (const fact of facts) {
1269
+ if (!lossFacts.has(fact)) continue;
1270
+ const loss = lossOf(fact);
1271
+ if (typeof loss === "string") skipped.push({
1272
+ fact: fact.id,
1273
+ reason: loss
1274
+ });
1275
+ else if (loss !== void 0) note("path.loss", loss.address, fact, binOf(PATH_LOSS_BINS, loss.share));
1276
+ }
1277
+ for (const fact of sorted.trace) skipped.push({
1278
+ fact: fact.id,
1279
+ reason: "The catalog has no feature for the hops of a path. The lab runs fit such a feature later."
1280
+ });
1281
+ return {
1282
+ evidence: [...slots.values()].map(({ feature, value, primary, facts: ids }) => ({
1283
+ feature,
1284
+ value,
1285
+ primary,
1286
+ facts: ids
1287
+ })),
1288
+ skipped
1289
+ };
1290
+ }
1291
+ /**
1292
+ * This function gives the share of the probes with an answer, for the feature `path.loss`. It gives a reason when a report
1293
+ * of `mtr` gives no evidence, and `undefined` when an echo fact gives no share.
1294
+ */
1295
+ function lossOf(fact) {
1296
+ if (isFactOf(ICMP_ECHO, fact)) {
1297
+ const echo = fact.value;
1298
+ return echo.received > 0 && echo.sent >= 20 ? {
1299
+ address: canonicalAddress(echo.address),
1300
+ share: echo.received / echo.sent
1301
+ } : void 0;
1302
+ }
1303
+ if (!isFactOf(PATH_REPORT, fact)) return void 0;
1304
+ const report = fact.value;
1305
+ const last = report.hops.at(-1);
1306
+ if (report.outcome !== "reached" || last === void 0) return `The outcome "${report.outcome}" of ${fact.kind} has no value of the feature path.loss.`;
1307
+ if (last.sent < 20) return `The last hop of the report has ${last.sent} probes. The feature path.loss needs 20 probes or more.`;
1308
+ return {
1309
+ address: canonicalAddress(report.address),
1310
+ share: last.received / last.sent
1311
+ };
1312
+ }
1313
+ function statusValue(response) {
1314
+ if (response.outcome === "error") return void 0;
1315
+ if (response.outcome !== "response") return "none";
1316
+ const status = response.status ?? 0;
1317
+ if (status >= 200 && status < 400) return "ok";
1318
+ if (status >= 400 && status < 500) return "client-error";
1319
+ if (status >= 500 && status < 600) return "server-error";
1320
+ }
1321
+ /**
1322
+ * This function gives the feature `ip.family` for a name with IPv4 and IPv6 addresses. It needs a TCP connect to an
1323
+ * address of each family.
1324
+ */
1325
+ function readFamily(sorted, primaryOf, note) {
1326
+ const names = /* @__PURE__ */ new Map();
1327
+ for (const fact of sorted.dns) {
1328
+ if (fact.value.outcome !== "ok") continue;
1329
+ const key = `${primaryOf(fact) ? 1 : 0}\u0000${fact.value.name.toLowerCase()}`;
1330
+ const entry = names.get(key) ?? {
1331
+ v4: /* @__PURE__ */ new Set(),
1332
+ v6: /* @__PURE__ */ new Set(),
1333
+ facts: [],
1334
+ primary: primaryOf(fact)
1335
+ };
1336
+ for (const address of fact.value.addresses) (fact.value.record === "A" ? entry.v4 : entry.v6).add(address);
1337
+ entry.facts.push(fact);
1338
+ names.set(key, entry);
1339
+ }
1340
+ for (const [key, entry] of names) {
1341
+ if (entry.v4.size === 0 || entry.v6.size === 0) continue;
1342
+ const tried = sorted.tcp.filter((tcp) => primaryOf(tcp) === entry.primary && (entry.v4.has(tcp.value.address) || entry.v6.has(tcp.value.address)));
1343
+ const works = (family) => {
1344
+ const connects = tried.filter((tcp) => family.has(tcp.value.address));
1345
+ return connects.length === 0 ? void 0 : connects.some((tcp) => tcp.value.outcome === "connected");
1346
+ };
1347
+ const v4 = works(entry.v4);
1348
+ const v6 = works(entry.v6);
1349
+ if (v4 === void 0 || v6 === void 0 || !v4 && !v6) continue;
1350
+ const value = v4 && v6 ? "both" : v4 ? "ipv4-only" : "ipv6-only";
1351
+ const [first, ...rest] = [...entry.facts, ...tried];
1352
+ if (first === void 0) continue;
1353
+ note("ip.family", key, first, value);
1354
+ for (const fact of rest) note("ip.family", key, fact, value);
1355
+ }
1356
+ }
1357
+ export { ADDRESS_CLASSES, CERT_PROBLEMS, CONNECT_OUTCOMES, DEFAULT_MAX_BYTES, DEFAULT_MAX_PORTS_PER_HOST, DEFAULT_TIMEOUT_MS, DNS_ANSWER, DNS_ANSWER_VALUES, DNS_OUTCOMES, DNS_PROBE, ECHO_OUTCOMES, FLOOR, GATEWAY_REPLY_VALUES, HANDSHAKE_OUTCOMES, HTTP_OUTCOMES, HTTP_PROBE, HTTP_RESPONSE, HTTP_TTFB_BINS, ICMP_ECHO, ICMP_REPLY_VALUES, KEPT_FIELDS, LAB_RECORD_FORMAT, LAYERS, LOCAL_SUFFIXES, LOSS_MIN_SENT, MIN_CONNECT_MS, NATIVE_KINDS, NONE, PARTS, PATH_LOSS_BINS, PATH_REPORT, PATH_TRACE, RECORD_TYPES, STANDARD_CATALOG, STANDARD_CATALOG_SPEC, STANDARD_FAULTS, STANDARD_FEATURES, STRENGTHS, TCP_CONNECT, TCP_CONNECT_VALUES, TCP_PROBE, TCP_TIME_BINS, TLS_HANDSHAKE, TLS_HANDSHAKE_VALUES, TLS_PROBE, TLS_RATIO_BINS, TLS_TRUST_VALUES, TOOL_KINDS, TRACE_OUTCOMES, TRUSTS, UNKNOWN, addressClass, addressFamily, addressKind, binOf, canonicalAddress, canonicalHost, certProblem, cidrContains, cidrSet, compileCatalog, connectOutcome, createRunner, decideScope, defineFactKind, defineProbe, diagnose, dnsOutcome, expand, explain, factsOf, formatCidr, formatTarget, grantProblems, handshakeOutcome, hostKind, httpProbe, isFactOf, isLayer, isLocalName, labGate, labRecordOf, layerRank, namedGrant, observe, parseCidr, parseTarget, predictive, readEvidence, resolvedGrant, sameTarget, scopeOf, scoreLab, target, tcpProbe, tlsProbe, widen };
1358
+
1359
+ //# sourceMappingURL=index.js.map