@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.
- package/README.md +153 -1
- package/build/address.d.ts +56 -0
- package/build/address.d.ts.map +1 -0
- package/build/address.js +172 -0
- package/build/address.js.map +1 -0
- package/build/{diagnosis-BTa-Dfwl.js → diagnosis-a4hRzFxp.js} +113 -11
- package/build/diagnosis-a4hRzFxp.js.map +1 -0
- package/build/diagnosis.d.ts +3 -2
- package/build/diagnosis.js +1 -1
- package/build/{index-CP1hzELI.d.ts → index-K2dGiYxW.d.ts} +83 -22
- package/build/index-K2dGiYxW.d.ts.map +1 -0
- package/build/index.d.ts +253 -148
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1084 -161
- package/build/index.js.map +1 -1
- package/build/kinds-CQv769lK.js +341 -0
- package/build/kinds-CQv769lK.js.map +1 -0
- package/build/kinds-_NShfZpU.d.ts +377 -0
- package/build/kinds-_NShfZpU.d.ts.map +1 -0
- package/build/layer-BNg0VYuK.d.ts +22 -0
- package/build/layer-BNg0VYuK.d.ts.map +1 -0
- package/build/tools.d.ts +206 -0
- package/build/tools.d.ts.map +1 -0
- package/build/tools.js +896 -0
- package/build/tools.js.map +1 -0
- package/package.json +17 -5
- package/src/address/index.ts +155 -0
- package/src/diagnosis/catalog.ts +8 -8
- package/src/diagnosis/index.ts +13 -0
- package/src/diagnosis/score.ts +160 -0
- package/src/index.ts +35 -0
- package/src/kinds.ts +460 -0
- package/src/probes/native.ts +354 -0
- package/src/probes/socket.ts +520 -0
- package/src/readers.ts +367 -0
- package/src/scope.ts +5 -24
- package/src/tools/common.ts +47 -0
- package/src/tools/host.ts +91 -0
- package/src/tools/index.ts +38 -0
- package/src/tools/licenses.ts +37 -0
- package/src/tools/mtr.ts +156 -0
- package/src/tools/ping.ts +231 -0
- package/src/tools/traceroute.ts +313 -0
- package/build/diagnosis-BTa-Dfwl.js.map +0 -1
- package/build/index-CP1hzELI.d.ts.map +0 -1
package/build/index.js
CHANGED
|
@@ -1,165 +1,19 @@
|
|
|
1
|
-
import { _ as
|
|
2
|
-
import {
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 =
|
|
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 =
|
|
232
|
-
if (range.tag === "
|
|
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
|
-
|
|
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
|