@mark1russell7/netdiag 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +153 -1
  2. package/build/address.d.ts +56 -0
  3. package/build/address.d.ts.map +1 -0
  4. package/build/address.js +172 -0
  5. package/build/address.js.map +1 -0
  6. package/build/{diagnosis-BTa-Dfwl.js → diagnosis-a4hRzFxp.js} +113 -11
  7. package/build/diagnosis-a4hRzFxp.js.map +1 -0
  8. package/build/diagnosis.d.ts +3 -2
  9. package/build/diagnosis.js +1 -1
  10. package/build/{index-CP1hzELI.d.ts → index-K2dGiYxW.d.ts} +83 -22
  11. package/build/index-K2dGiYxW.d.ts.map +1 -0
  12. package/build/index.d.ts +253 -148
  13. package/build/index.d.ts.map +1 -1
  14. package/build/index.js +1084 -161
  15. package/build/index.js.map +1 -1
  16. package/build/kinds-CQv769lK.js +341 -0
  17. package/build/kinds-CQv769lK.js.map +1 -0
  18. package/build/kinds-_NShfZpU.d.ts +377 -0
  19. package/build/kinds-_NShfZpU.d.ts.map +1 -0
  20. package/build/layer-BNg0VYuK.d.ts +22 -0
  21. package/build/layer-BNg0VYuK.d.ts.map +1 -0
  22. package/build/tools.d.ts +206 -0
  23. package/build/tools.d.ts.map +1 -0
  24. package/build/tools.js +896 -0
  25. package/build/tools.js.map +1 -0
  26. package/package.json +17 -5
  27. package/src/address/index.ts +155 -0
  28. package/src/diagnosis/catalog.ts +8 -8
  29. package/src/diagnosis/index.ts +13 -0
  30. package/src/diagnosis/score.ts +160 -0
  31. package/src/index.ts +35 -0
  32. package/src/kinds.ts +460 -0
  33. package/src/probes/native.ts +354 -0
  34. package/src/probes/socket.ts +520 -0
  35. package/src/readers.ts +367 -0
  36. package/src/scope.ts +5 -24
  37. package/src/tools/common.ts +47 -0
  38. package/src/tools/host.ts +91 -0
  39. package/src/tools/index.ts +38 -0
  40. package/src/tools/licenses.ts +37 -0
  41. package/src/tools/mtr.ts +156 -0
  42. package/src/tools/ping.ts +231 -0
  43. package/src/tools/traceroute.ts +313 -0
  44. package/build/diagnosis-BTa-Dfwl.js.map +0 -1
  45. package/build/index-CP1hzELI.d.ts.map +0 -1
package/build/index.js CHANGED
@@ -1,165 +1,19 @@
1
- import { _ as layerRank, a as FLOOR, c as STRENGTHS, d as diagnose, f as expand, g as isLayer, h as LAYERS, i as STANDARD_FEATURES, l as UNKNOWN, m as predictive, n as STANDARD_CATALOG_SPEC, o as NONE, p as explain, r as STANDARD_FAULTS, s as PARTS, t as STANDARD_CATALOG, u as compileCatalog } from "./diagnosis-BTa-Dfwl.js";
2
- import { BlockList, isIP } from "node:net";
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";
3
5
  import { err, ok } from "@mark1russell7/optional";
4
- // src/target.ts
5
- /** The ports of the URL schemes that a target can have. */
6
- const SCHEME_PORTS = /* @__PURE__ */ new Map([
7
- ["http:", 80],
8
- ["https:", 443],
9
- ["ws:", 80],
10
- ["wss:", 443],
11
- ["dns:", 53],
12
- ["tls:", 443]
13
- ]);
14
- /** This function gives the kind of a host in its canonical form. */
15
- function hostKind(host) {
16
- const family = isIP(host);
17
- if (family === 4) return "ipv4";
18
- if (family === 6) return "ipv6";
19
- return "name";
20
- }
21
- /** This function gives the canonical form of a host: lower case, with no final dot and no brackets. */
22
- function canonicalHost(host) {
23
- let text = host.trim().toLowerCase();
24
- if (text.startsWith("[") && text.endsWith("]")) text = text.slice(1, -1);
25
- if (text.endsWith(".")) text = text.slice(0, -1);
26
- return text;
27
- }
28
- /** This function makes a target from a host and an optional port. */
29
- function target(host, port) {
30
- const canonical = canonicalHost(host);
31
- return port === void 0 ? {
32
- host: canonical,
33
- kind: hostKind(canonical)
34
- } : {
35
- host: canonical,
36
- kind: hostKind(canonical),
37
- port
38
- };
39
- }
40
- /** This function gives the text of a target: `host`, `host:port`, or `[address]:port` for an IPv6 address. */
41
- function formatTarget(t) {
42
- const host = t.kind === "ipv6" && t.port !== void 0 ? `[${t.host}]` : t.host;
43
- return t.port === void 0 ? host : `${host}:${t.port}`;
44
- }
45
- /** This function tells if two targets have the same host and the same port. */
46
- function sameTarget(a, b) {
47
- return a.host === b.host && a.port === b.port;
48
- }
49
- const LABEL = /^(?!-)[a-z0-9-]{1,63}(?<!-)$/;
50
- function validName(host) {
51
- if (host.length === 0 || host.length > 253) return false;
52
- return host.split(".").every((label) => LABEL.test(label) || label === "_" || /^_[a-z0-9-]{1,62}$/.test(label));
53
- }
54
- function portOf(text) {
55
- if (!/^\d{1,5}$/.test(text)) return void 0;
56
- const port = Number(text);
57
- return port >= 1 && port <= 65535 ? port : void 0;
58
- }
59
- /**
60
- * This function reads a target from the text that a user gives.
61
- *
62
- * It accepts a URL (`https://example.com/a`), a host (`example.com`) and a host with a port (`example.com:8443`).
63
- * It also accepts an IPv4 address, and an IPv6 address in brackets with a port (`[2001:db8::1]:443`).
64
- * A URL with a known scheme and no port gets the port of its scheme.
65
- */
66
- function parseTarget(text) {
67
- const input = text.trim();
68
- if (input.length === 0) return err("The target is empty.");
69
- if (input.includes("://")) {
70
- let url;
71
- try {
72
- url = new URL(input);
73
- } catch {
74
- return err(`"${input}" is not a valid URL.`);
75
- }
76
- const host = canonicalHost(url.hostname);
77
- if (host.length === 0) return err(`The URL "${input}" has no host.`);
78
- return checked(host, url.port === "" ? SCHEME_PORTS.get(url.protocol) : Number(url.port), input);
79
- }
80
- if (isIP(input) === 6) return checked(canonicalHost(input), void 0, input);
81
- const bracket = /^\[([^\]]+)\](?::(.*))?$/.exec(input);
82
- if (bracket !== null) {
83
- const host = bracket[1] ?? "";
84
- if (isIP(host) !== 6) return err(`"${host}" is not an IPv6 address.`);
85
- if (bracket[2] === void 0) return checked(canonicalHost(host), void 0, input);
86
- const port = portOf(bracket[2]);
87
- return port === void 0 ? err(`"${bracket[2]}" is not a port from 1 to 65535.`) : checked(canonicalHost(host), port, input);
88
- }
89
- const colon = input.lastIndexOf(":");
90
- if (colon === -1) return checked(canonicalHost(input), void 0, input);
91
- const port = portOf(input.slice(colon + 1));
92
- if (port === void 0) return err(`"${input.slice(colon + 1)}" is not a port from 1 to 65535.`);
93
- return checked(canonicalHost(input.slice(0, colon)), port, input);
94
- }
95
- function checked(host, port, input) {
96
- const kind = hostKind(host);
97
- if (kind === "name" && !validName(host)) return err(`"${input}" has no valid host name.`);
98
- return ok(port === void 0 ? {
99
- host,
100
- kind
101
- } : {
102
- host,
103
- kind,
104
- port
105
- });
106
- }
107
- // src/fact.ts
108
- const KIND_NAME = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
109
- /** This function makes a fact kind. The name has lower-case words with dots, for example `tls.handshake`. */
110
- function defineFactKind(spec) {
111
- if (!KIND_NAME.test(spec.kind)) throw new RangeError(`"${spec.kind}" is not a valid name of a fact kind`);
112
- return Object.freeze({ ...spec });
113
- }
114
- /** This function tells if a fact is of a kind, and if its value passes the check of the kind. */
115
- function isFactOf(kind, fact) {
116
- return fact.kind === kind.kind && fact.layer === kind.layer && kind.check(fact.value);
117
- }
118
- /** This function gives the facts of one kind, in their order. */
119
- function factsOf(kind, facts) {
120
- return facts.filter((fact) => isFactOf(kind, fact));
121
- }
122
- /** This function makes an observation with a type check of its value against its kind. */
123
- function observe(kind, subject, value, time) {
124
- return time === void 0 ? {
125
- kind,
126
- subject,
127
- value
128
- } : {
129
- kind,
130
- subject,
131
- value,
132
- time
133
- };
134
- }
135
- // src/probe.ts
136
- /** This function makes a probe, and examines its fields. */
137
- function defineProbe(probe) {
138
- if (!/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(probe.id)) throw new RangeError(`"${probe.id}" is not a valid probe id`);
139
- if (probe.kinds.length === 0) throw new RangeError(`The probe "${probe.id}" gives no kind of fact`);
140
- return probe;
141
- }
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";
142
10
  // src/scope.ts
143
11
  /** The default limit of the different ports of one host in one runner. */
144
12
  const DEFAULT_MAX_PORTS_PER_HOST = 8;
145
- function parseRange(cidr) {
146
- const slash = cidr.indexOf("/");
147
- const address = canonicalHost(slash === -1 ? cidr : cidr.slice(0, slash));
148
- const family = isIP(address);
149
- if (family === 0) return err(`"${cidr}" is not an IP address or a CIDR range.`);
150
- const max = family === 4 ? 32 : 128;
151
- const prefixText = slash === -1 ? String(max) : cidr.slice(slash + 1);
152
- if (!/^\d{1,3}$/.test(prefixText) || Number(prefixText) > max) return err(`"${cidr}" has no prefix from 0 to ${max}.`);
153
- return ok({
154
- address,
155
- prefix: Number(prefixText),
156
- family: family === 4 ? "ipv4" : "ipv6"
157
- });
158
- }
159
13
  /** This function gives the problems of a grant, or an empty list for a correct grant. */
160
14
  function grantProblems(grant) {
161
15
  if (grant.kind === "range") {
162
- const range = parseRange(grant.cidr);
16
+ const range = parseCidr(grant.cidr);
163
17
  return range.tag === "err" ? [range.error] : [];
164
18
  }
165
19
  const host = canonicalHost(grant.host);
@@ -228,11 +82,8 @@ function grantFor(scope, peer) {
228
82
  continue;
229
83
  }
230
84
  if (family === 0) continue;
231
- const range = parseRange(grant.cidr);
232
- if (range.tag === "err" || range.value.family === "ipv4" !== (family === 4)) continue;
233
- const list = new BlockList();
234
- list.addSubnet(range.value.address, range.value.prefix, range.value.family);
235
- if (list.check(host, family === 4 ? "ipv4" : "ipv6")) return grant;
85
+ const range = parseCidr(grant.cidr);
86
+ if (range.tag === "ok" && cidrContains(range.value, host)) return grant;
236
87
  }
237
88
  }
238
89
  /**
@@ -431,6 +282,1078 @@ function createRunner(options) {
431
282
  run
432
283
  };
433
284
  }
434
- export { DEFAULT_MAX_PORTS_PER_HOST, FLOOR, LAYERS, NONE, PARTS, STANDARD_CATALOG, STANDARD_CATALOG_SPEC, STANDARD_FAULTS, STANDARD_FEATURES, STRENGTHS, UNKNOWN, canonicalHost, compileCatalog, createRunner, decideScope, defineFactKind, defineProbe, diagnose, expand, explain, factsOf, formatTarget, grantProblems, hostKind, isFactOf, isLayer, layerRank, namedGrant, observe, parseTarget, predictive, resolvedGrant, sameTarget, scopeOf, target, widen };
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 };
435
1358
 
436
1359
  //# sourceMappingURL=index.js.map