@mark1russell7/netdiag 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +244 -2
  3. package/build/address.d.ts +56 -0
  4. package/build/address.d.ts.map +1 -0
  5. package/build/address.js +172 -0
  6. package/build/address.js.map +1 -0
  7. package/build/diagnosis-a4hRzFxp.js +922 -0
  8. package/build/diagnosis-a4hRzFxp.js.map +1 -0
  9. package/build/diagnosis.d.ts +8 -0
  10. package/build/diagnosis.js +1 -0
  11. package/build/index-K2dGiYxW.d.ts +259 -0
  12. package/build/index-K2dGiYxW.d.ts.map +1 -0
  13. package/build/index.d.ts +392 -0
  14. package/build/index.d.ts.map +1 -0
  15. package/build/index.js +1359 -0
  16. package/build/index.js.map +1 -0
  17. package/build/kinds-CQv769lK.js +341 -0
  18. package/build/kinds-CQv769lK.js.map +1 -0
  19. package/build/kinds-_NShfZpU.d.ts +377 -0
  20. package/build/kinds-_NShfZpU.d.ts.map +1 -0
  21. package/build/layer-BNg0VYuK.d.ts +22 -0
  22. package/build/layer-BNg0VYuK.d.ts.map +1 -0
  23. package/build/tools.d.ts +206 -0
  24. package/build/tools.d.ts.map +1 -0
  25. package/build/tools.js +896 -0
  26. package/build/tools.js.map +1 -0
  27. package/package.json +81 -3
  28. package/src/address/index.ts +155 -0
  29. package/src/diagnosis/catalog.ts +357 -0
  30. package/src/diagnosis/index.ts +47 -0
  31. package/src/diagnosis/model.ts +322 -0
  32. package/src/diagnosis/score.ts +160 -0
  33. package/src/fact.ts +87 -0
  34. package/src/index.ts +68 -0
  35. package/src/kinds.ts +460 -0
  36. package/src/layer.ts +31 -0
  37. package/src/probe.ts +61 -0
  38. package/src/probes/native.ts +354 -0
  39. package/src/probes/socket.ts +520 -0
  40. package/src/readers.ts +367 -0
  41. package/src/run.ts +163 -0
  42. package/src/scope.ts +152 -0
  43. package/src/target.ts +116 -0
  44. package/src/tools/common.ts +47 -0
  45. package/src/tools/host.ts +91 -0
  46. package/src/tools/index.ts +38 -0
  47. package/src/tools/licenses.ts +37 -0
  48. package/src/tools/mtr.ts +156 -0
  49. package/src/tools/ping.ts +231 -0
  50. package/src/tools/traceroute.ts +313 -0
@@ -0,0 +1,354 @@
1
+ import { Resolver } from "node:dns/promises";
2
+ import { isIP } from "node:net";
3
+ import { observe, type Observation } from "../fact.ts";
4
+ import { DNS_ANSWER, TCP_CONNECT, TLS_HANDSHAKE, HTTP_RESPONSE, type DnsOutcome, type RecordType } from "../kinds.ts";
5
+ import { defineProbe, type Availability, type Probe } from "../probe.ts";
6
+ import { formatTarget, target } from "../target.ts";
7
+ import {
8
+ connectPhase,
9
+ dialOf,
10
+ exchangePhase,
11
+ handshakePhase,
12
+ httpObservation,
13
+ peerOf,
14
+ tcpObservation,
15
+ timeoutOf,
16
+ tlsObservation,
17
+ trustStore,
18
+ watch,
19
+ codeOf,
20
+ type ConnectOptions,
21
+ type HttpPhase,
22
+ type TcpPhase,
23
+ type TlsPhase,
24
+ type TrustStore,
25
+ } from "./socket.ts";
26
+
27
+ // The native probes use only the `node:` modules. Each network failure is an outcome in the raw output, not an error.
28
+
29
+ const NATIVE: Availability = { available: true, version: process.versions.node };
30
+
31
+ function native(): Promise<Availability> {
32
+ return Promise.resolve(NATIVE);
33
+ }
34
+
35
+ /** The input of the DNS probe: one query for the addresses of a name, to one resolver. */
36
+ export interface DnsInput {
37
+ readonly name: string;
38
+ /** The record type. The default is `A`. */
39
+ readonly record?: RecordType | undefined;
40
+ /** The IP address of the resolver. */
41
+ readonly resolver: string;
42
+ /** The port of the resolver. The default is 53. */
43
+ readonly port?: number | undefined;
44
+ readonly timeoutMs?: number | undefined;
45
+ }
46
+
47
+ /** The raw output of the DNS probe. The times are readings of the clock of the runner. */
48
+ export interface DnsRaw {
49
+ readonly name: string;
50
+ readonly record: RecordType;
51
+ readonly resolver: string;
52
+ readonly start: number;
53
+ readonly end: number;
54
+ readonly outcome: DnsOutcome;
55
+ readonly addresses: readonly string[];
56
+ readonly ttl?: number | undefined;
57
+ readonly code?: string | undefined;
58
+ }
59
+
60
+ /** This function maps an error code of a DNS query to its outcome. */
61
+ export function dnsOutcome(code: string | undefined): DnsOutcome {
62
+ switch (code) {
63
+ case "ENOTFOUND":
64
+ return "nxdomain";
65
+ case "ENODATA":
66
+ return "nodata";
67
+ case "ESERVFAIL":
68
+ return "servfail";
69
+ case "EREFUSED":
70
+ return "refused";
71
+ case "ETIMEOUT":
72
+ return "timeout";
73
+ case "ECONNREFUSED":
74
+ return "unreachable";
75
+ default:
76
+ return "error";
77
+ }
78
+ }
79
+
80
+ /**
81
+ * The DNS probe sends one query for the addresses of a name to one resolver. Its peer is the resolver, not the name.
82
+ * The query uses no search domain.
83
+ */
84
+ export const DNS_PROBE: Probe<DnsInput, DnsRaw> = defineProbe<DnsInput, DnsRaw>({
85
+ id: "dns.query",
86
+ version: "1",
87
+ summary: "One DNS query for the A or AAAA records of a name, to one resolver.",
88
+ layer: "dns",
89
+ mode: "active",
90
+ kinds: [DNS_ANSWER],
91
+ peers(input) {
92
+ if (isIP(input.name) !== 0) throw new TypeError(`"${input.name}" is an address, not a name.`);
93
+ return [peerOf(input.resolver, input.port ?? 53)];
94
+ },
95
+ available: native,
96
+ async run(input, context) {
97
+ const record = input.record ?? "A";
98
+ const limit = timeoutOf(input.timeoutMs);
99
+ const server = formatTarget(target(input.resolver, input.port ?? 53));
100
+ const resolver = new Resolver({ timeout: limit, tries: 1 });
101
+ resolver.setServers([server]);
102
+ let expired = false;
103
+ const stop = (): void => resolver.cancel();
104
+ const cancel = context.clock.after(limit, () => {
105
+ expired = true;
106
+ stop();
107
+ });
108
+ context.signal.addEventListener("abort", stop, { once: true });
109
+ const start = context.clock.now();
110
+ const base = { name: input.name, record, resolver: server, start };
111
+ try {
112
+ const answers = record === "A" ? await resolver.resolve4(input.name, { ttl: true }) : await resolver.resolve6(input.name, { ttl: true });
113
+ const raw: DnsRaw = {
114
+ ...base,
115
+ end: context.clock.now(),
116
+ outcome: "ok",
117
+ addresses: answers.map((answer) => answer.address),
118
+ ...(answers.length === 0 ? {} : { ttl: Math.min(...answers.map((answer) => answer.ttl)) }),
119
+ };
120
+ context.signal.throwIfAborted();
121
+ return raw;
122
+ } catch (thrown) {
123
+ context.signal.throwIfAborted();
124
+ const code = codeOf(thrown);
125
+ return { ...base, end: context.clock.now(), outcome: expired ? "timeout" : dnsOutcome(code), addresses: [], ...(code === undefined ? {} : { code }) };
126
+ } finally {
127
+ cancel();
128
+ context.signal.removeEventListener("abort", stop);
129
+ }
130
+ },
131
+ parse(raw) {
132
+ const { start, end, ...rest } = raw;
133
+ return [observe(DNS_ANSWER, target(raw.name), { ...rest, ms: end - start }, { start, end })];
134
+ },
135
+ });
136
+
137
+ /** The input of the TCP probe: one connect to an IP address and a port. */
138
+ export interface TcpInput {
139
+ readonly address: string;
140
+ readonly port: number;
141
+ readonly timeoutMs?: number | undefined;
142
+ }
143
+
144
+ /** The raw output of the TCP probe. */
145
+ export interface TcpRaw {
146
+ readonly tcp: TcpPhase;
147
+ }
148
+
149
+ /** This function makes the TCP probe. It connects, and then it closes the connection. */
150
+ export function tcpProbe(options: ConnectOptions = {}): Probe<TcpInput, TcpRaw> {
151
+ const dial = dialOf(options);
152
+ return defineProbe<TcpInput, TcpRaw>({
153
+ id: "tcp.connect",
154
+ version: "1",
155
+ summary: "One TCP connect to an IP address and a port.",
156
+ layer: "tcp",
157
+ mode: "active",
158
+ kinds: [TCP_CONNECT],
159
+ peers: (input) => [peerOf(input.address, input.port)],
160
+ available: native,
161
+ async run(input, context) {
162
+ const w = watch(context, timeoutOf(input.timeoutMs));
163
+ try {
164
+ const { socket, phase } = await connectPhase(dial, input.address, input.port, w, context.clock);
165
+ socket?.destroy();
166
+ context.signal.throwIfAborted();
167
+ return { tcp: phase };
168
+ } finally {
169
+ w.close();
170
+ }
171
+ },
172
+ parse: (raw) => [tcpObservation(raw.tcp)],
173
+ });
174
+ }
175
+
176
+ /** The TCP probe with the `connect` of `node:net`. */
177
+ export const TCP_PROBE: Probe<TcpInput, TcpRaw> = tcpProbe();
178
+
179
+ /** The input of the TLS probe: a TCP connect and a TLS handshake. */
180
+ export interface TlsInput {
181
+ readonly address: string;
182
+ /** The port. The default is 443. */
183
+ readonly port?: number | undefined;
184
+ /** The name for SNI and for the check of the certificate. */
185
+ readonly servername?: string | undefined;
186
+ readonly alpn?: readonly string[] | undefined;
187
+ readonly timeoutMs?: number | undefined;
188
+ }
189
+
190
+ /** The raw output of the TLS probe. It has no TLS phase when the TCP connect failed. */
191
+ export interface TlsRaw {
192
+ readonly tcp: TcpPhase;
193
+ readonly tls?: TlsPhase | undefined;
194
+ }
195
+
196
+ /** The options of the TLS and HTTP probes. */
197
+ export interface TlsProbeOptions extends ConnectOptions {
198
+ /** More roots of trust, as PEM text: the roots that the user trusts, for example the root of a company. */
199
+ readonly roots?: readonly string[] | undefined;
200
+ /**
201
+ * More public roots, as PEM text. A chain that ends at one of them gets the trust `public`. A lab gives its own
202
+ * root here, so that its servers stand for servers on the internet.
203
+ */
204
+ readonly publicRoots?: readonly string[] | undefined;
205
+ }
206
+
207
+ function lazyStore(options: TlsProbeOptions): () => TrustStore {
208
+ let store: TrustStore | undefined;
209
+ return () => (store ??= trustStore(options.roots ?? [], options.publicRoots ?? []));
210
+ }
211
+
212
+ /**
213
+ * This function makes the TLS probe. It measures the TCP connect and the TLS handshake apart. It examines the
214
+ * certificate, and it does not refuse a certificate that is not valid.
215
+ */
216
+ export function tlsProbe(options: TlsProbeOptions = {}): Probe<TlsInput, TlsRaw> {
217
+ const dial = dialOf(options);
218
+ const store = lazyStore(options);
219
+ return defineProbe<TlsInput, TlsRaw>({
220
+ id: "tls.handshake",
221
+ version: "1",
222
+ summary: "One TCP connect and one TLS handshake, with the certificate of the server.",
223
+ layer: "tls",
224
+ mode: "active",
225
+ kinds: [TCP_CONNECT, TLS_HANDSHAKE],
226
+ peers: (input) => [peerOf(input.address, input.port ?? 443)],
227
+ available: native,
228
+ async run(input, context) {
229
+ const port = input.port ?? 443;
230
+ const w = watch(context, timeoutOf(input.timeoutMs));
231
+ try {
232
+ const { socket, phase } = await connectPhase(dial, input.address, port, w, context.clock);
233
+ if (socket === undefined) {
234
+ context.signal.throwIfAborted();
235
+ return { tcp: phase };
236
+ }
237
+ const handshake = await handshakePhase(socket, { address: input.address, servername: input.servername, alpn: input.alpn }, store(), w, context.clock);
238
+ handshake.secure?.destroy();
239
+ context.signal.throwIfAborted();
240
+ return { tcp: phase, tls: handshake.phase };
241
+ } finally {
242
+ w.close();
243
+ }
244
+ },
245
+ parse(raw) {
246
+ const observations: Observation[] = [tcpObservation(raw.tcp)];
247
+ if (raw.tls !== undefined) observations.push(tlsObservation(raw.tcp.address, raw.tcp.port, raw.tls));
248
+ return observations;
249
+ },
250
+ });
251
+ }
252
+
253
+ /** The TLS probe with the public roots and the roots of the computer. */
254
+ export const TLS_PROBE: Probe<TlsInput, TlsRaw> = tlsProbe();
255
+
256
+ /** The input of the HTTP probe: one HTTP/1.1 request over TCP, or over TLS for `https`. */
257
+ export interface HttpInput {
258
+ readonly address: string;
259
+ readonly scheme: "http" | "https";
260
+ /** The port. The default is 80 for `http` and 443 for `https`. */
261
+ readonly port?: number | undefined;
262
+ /** The name of the host for the `Host` field and for SNI. The default is the address. */
263
+ readonly host?: string | undefined;
264
+ /** The path of the request. The default is `/`. */
265
+ readonly path?: string | undefined;
266
+ /** The method. The default is `GET`. */
267
+ readonly method?: "GET" | "HEAD" | undefined;
268
+ readonly timeoutMs?: number | undefined;
269
+ /** The limit of the bytes of the body that the probe reads. The default is 65536. */
270
+ readonly maxBytes?: number | undefined;
271
+ }
272
+
273
+ /** The raw output of the HTTP probe. */
274
+ export interface HttpRaw {
275
+ readonly scheme: "http" | "https";
276
+ readonly tcp: TcpPhase;
277
+ readonly tls?: TlsPhase | undefined;
278
+ readonly http?: HttpPhase | undefined;
279
+ }
280
+
281
+ /** The limit of the bytes of a body that the HTTP probe reads, when its input gives none: 64 KiB. */
282
+ export const DEFAULT_MAX_BYTES = 65536;
283
+
284
+ function hostField(input: HttpInput, port: number): string {
285
+ const host = input.host ?? (isIP(input.address) === 6 ? `[${input.address}]` : input.address);
286
+ const standard = input.scheme === "https" ? 443 : 80;
287
+ return port === standard ? host : `${host}:${port}`;
288
+ }
289
+
290
+ /**
291
+ * This function makes the HTTP probe. It sends one request, follows no redirect, and reads the body up to a limit. It
292
+ * gives the facts of each phase: TCP, TLS for `https`, and HTTP. For `https`, it sends the request only when the
293
+ * certificate passes its check.
294
+ */
295
+ export function httpProbe(options: TlsProbeOptions = {}): Probe<HttpInput, HttpRaw> {
296
+ const dial = dialOf(options);
297
+ const store = lazyStore(options);
298
+ const portOf = (input: HttpInput): number => input.port ?? (input.scheme === "https" ? 443 : 80);
299
+ return defineProbe<HttpInput, HttpRaw>({
300
+ id: "http.request",
301
+ version: "1",
302
+ summary: "One HTTP/1.1 request, with the time of each phase: TCP, TLS and the first byte.",
303
+ layer: "http",
304
+ mode: "active",
305
+ kinds: [TCP_CONNECT, TLS_HANDSHAKE, HTTP_RESPONSE],
306
+ peers(input) {
307
+ const path = input.path ?? "/";
308
+ if (!path.startsWith("/") || /[\s]/.test(path)) throw new TypeError(`"${path}" is not a path that starts with "/" and has no space.`);
309
+ return [peerOf(input.address, portOf(input))];
310
+ },
311
+ available: native,
312
+ async run(input, context) {
313
+ const port = portOf(input);
314
+ const maxBytes = input.maxBytes ?? DEFAULT_MAX_BYTES;
315
+ if (!Number.isInteger(maxBytes) || maxBytes < 1) throw new RangeError(`The limit of bytes ${maxBytes} is not a positive integer.`);
316
+ const w = watch(context, timeoutOf(input.timeoutMs));
317
+ try {
318
+ const { socket, phase: tcp } = await connectPhase(dial, input.address, port, w, context.clock);
319
+ if (socket === undefined) {
320
+ context.signal.throwIfAborted();
321
+ return { scheme: input.scheme, tcp };
322
+ }
323
+ let stream = socket;
324
+ let secured: TlsPhase | undefined;
325
+ if (input.scheme === "https") {
326
+ const handshake = await handshakePhase(socket, { address: input.address, servername: input.host, alpn: ["http/1.1"] }, store(), w, context.clock);
327
+ secured = handshake.phase;
328
+ // A request on a session with a certificate that is not valid can send the path to an interceptor.
329
+ if (handshake.secure === undefined || secured.outcome !== "ok") {
330
+ handshake.secure?.destroy();
331
+ context.signal.throwIfAborted();
332
+ return { scheme: input.scheme, tcp, tls: secured };
333
+ }
334
+ stream = handshake.secure;
335
+ }
336
+ const http = await exchangePhase(stream, { method: input.method ?? "GET", host: hostField(input, port), path: input.path ?? "/", maxBytes }, w, context.clock);
337
+ context.signal.throwIfAborted();
338
+ return { scheme: input.scheme, tcp, ...(secured === undefined ? {} : { tls: secured }), http };
339
+ } finally {
340
+ w.close();
341
+ }
342
+ },
343
+ parse(raw) {
344
+ const { address, port } = raw.tcp;
345
+ const observations: Observation[] = [tcpObservation(raw.tcp)];
346
+ if (raw.tls !== undefined) observations.push(tlsObservation(address, port, raw.tls));
347
+ if (raw.http !== undefined) observations.push(httpObservation(address, port, raw.scheme, raw.http));
348
+ return observations;
349
+ },
350
+ });
351
+ }
352
+
353
+ /** The HTTP probe with the public roots and the roots of the computer. */
354
+ export const HTTP_PROBE: Probe<HttpInput, HttpRaw> = httpProbe();