@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.
- package/LICENSE +21 -0
- package/README.md +244 -2
- 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-a4hRzFxp.js +922 -0
- package/build/diagnosis-a4hRzFxp.js.map +1 -0
- package/build/diagnosis.d.ts +8 -0
- package/build/diagnosis.js +1 -0
- package/build/index-K2dGiYxW.d.ts +259 -0
- package/build/index-K2dGiYxW.d.ts.map +1 -0
- package/build/index.d.ts +392 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +1359 -0
- package/build/index.js.map +1 -0
- 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 +81 -3
- package/src/address/index.ts +155 -0
- package/src/diagnosis/catalog.ts +357 -0
- package/src/diagnosis/index.ts +47 -0
- package/src/diagnosis/model.ts +322 -0
- package/src/diagnosis/score.ts +160 -0
- package/src/fact.ts +87 -0
- package/src/index.ts +68 -0
- package/src/kinds.ts +460 -0
- package/src/layer.ts +31 -0
- package/src/probe.ts +61 -0
- package/src/probes/native.ts +354 -0
- package/src/probes/socket.ts +520 -0
- package/src/readers.ts +367 -0
- package/src/run.ts +163 -0
- package/src/scope.ts +152 -0
- package/src/target.ts +116 -0
- 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/src/kinds.ts
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { defineFactKind, type FactKind } from "./fact.ts";
|
|
2
|
+
|
|
3
|
+
// The fact kinds of the native probes. This module imports no `node:` module, so a browser can read a record of facts.
|
|
4
|
+
|
|
5
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isText(value: unknown): value is string {
|
|
10
|
+
return typeof value === "string" && value.length > 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isDuration(value: unknown): value is number {
|
|
14
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isPort(value: unknown): value is number {
|
|
18
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isCount(value: unknown): value is number {
|
|
22
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isOneOf<T extends string>(values: readonly T[], value: unknown): value is T {
|
|
26
|
+
return typeof value === "string" && (values as readonly string[]).includes(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function optional(value: unknown, check: (value: unknown) => boolean): boolean {
|
|
30
|
+
return value === undefined || check(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isTextList(value: unknown): value is readonly string[] {
|
|
34
|
+
return Array.isArray(value) && value.every(isText);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The outcomes of a DNS query. `nodata` tells that the name exists, but it has no record of the type. */
|
|
38
|
+
export const DNS_OUTCOMES = ["ok", "nodata", "nxdomain", "servfail", "refused", "timeout", "unreachable", "error"] as const;
|
|
39
|
+
export type DnsOutcome = (typeof DNS_OUTCOMES)[number];
|
|
40
|
+
|
|
41
|
+
/** The record types of a DNS query for addresses. */
|
|
42
|
+
export const RECORD_TYPES = ["A", "AAAA"] as const;
|
|
43
|
+
export type RecordType = (typeof RECORD_TYPES)[number];
|
|
44
|
+
|
|
45
|
+
/** The answer of a DNS resolver to one query. */
|
|
46
|
+
export interface DnsAnswer {
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly record: RecordType;
|
|
49
|
+
/** The resolver, as `address:port`. */
|
|
50
|
+
readonly resolver: string;
|
|
51
|
+
readonly outcome: DnsOutcome;
|
|
52
|
+
readonly addresses: readonly string[];
|
|
53
|
+
/** The lowest TTL of the records, in seconds. */
|
|
54
|
+
readonly ttl?: number | undefined;
|
|
55
|
+
/** The time from the query to the answer or to the failure, in milliseconds. */
|
|
56
|
+
readonly ms: number;
|
|
57
|
+
/** The error code of the platform for a failure, for example `ENOTFOUND`. */
|
|
58
|
+
readonly code?: string | undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The fact kind `dns.answer`. */
|
|
62
|
+
export const DNS_ANSWER: FactKind<DnsAnswer> = defineFactKind<DnsAnswer>({
|
|
63
|
+
kind: "dns.answer",
|
|
64
|
+
layer: "dns",
|
|
65
|
+
summary: "The answer of a DNS resolver to one query for the addresses of a name.",
|
|
66
|
+
check: (value): value is DnsAnswer =>
|
|
67
|
+
isRecord(value) &&
|
|
68
|
+
isText(value["name"]) &&
|
|
69
|
+
isOneOf(RECORD_TYPES, value["record"]) &&
|
|
70
|
+
isText(value["resolver"]) &&
|
|
71
|
+
isOneOf(DNS_OUTCOMES, value["outcome"]) &&
|
|
72
|
+
isTextList(value["addresses"]) &&
|
|
73
|
+
(value["outcome"] !== "ok" || value["addresses"].length > 0) &&
|
|
74
|
+
optional(value["ttl"], isDuration) &&
|
|
75
|
+
isDuration(value["ms"]) &&
|
|
76
|
+
optional(value["code"], isText),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/** The outcomes of a TCP connect. */
|
|
80
|
+
export const CONNECT_OUTCOMES = ["connected", "refused", "timeout", "unreachable", "reset", "error"] as const;
|
|
81
|
+
export type ConnectOutcome = (typeof CONNECT_OUTCOMES)[number];
|
|
82
|
+
|
|
83
|
+
/** The outcome of one TCP connect. */
|
|
84
|
+
export interface TcpConnect {
|
|
85
|
+
readonly address: string;
|
|
86
|
+
readonly port: number;
|
|
87
|
+
readonly family: 4 | 6;
|
|
88
|
+
readonly outcome: ConnectOutcome;
|
|
89
|
+
/** The time from the start of the connect to the connection or to the failure, in milliseconds. */
|
|
90
|
+
readonly ms: number;
|
|
91
|
+
readonly code?: string | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The fact kind `tcp.connect`. */
|
|
95
|
+
export const TCP_CONNECT: FactKind<TcpConnect> = defineFactKind<TcpConnect>({
|
|
96
|
+
kind: "tcp.connect",
|
|
97
|
+
layer: "tcp",
|
|
98
|
+
summary: "The outcome and the time of one TCP connect to an address and a port.",
|
|
99
|
+
check: (value): value is TcpConnect =>
|
|
100
|
+
isRecord(value) &&
|
|
101
|
+
isText(value["address"]) &&
|
|
102
|
+
isPort(value["port"]) &&
|
|
103
|
+
(value["family"] === 4 || value["family"] === 6) &&
|
|
104
|
+
isOneOf(CONNECT_OUTCOMES, value["outcome"]) &&
|
|
105
|
+
isDuration(value["ms"]) &&
|
|
106
|
+
optional(value["code"], isText),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The outcomes of a TLS handshake.
|
|
111
|
+
*
|
|
112
|
+
* - `cert-error`: the handshake ended, but the certificate is not valid for the name.
|
|
113
|
+
* - `alert`: the client and the server stopped the handshake, for example because they have no version in common.
|
|
114
|
+
* - `not-tls`: the server does not speak TLS on the port.
|
|
115
|
+
* - `closed`: the server closed the connection before the end of the handshake.
|
|
116
|
+
*/
|
|
117
|
+
export const HANDSHAKE_OUTCOMES = ["ok", "cert-error", "alert", "not-tls", "timeout", "reset", "closed", "error"] as const;
|
|
118
|
+
export type HandshakeOutcome = (typeof HANDSHAKE_OUTCOMES)[number];
|
|
119
|
+
|
|
120
|
+
/** The roots of trust that a certificate chain ends at: a public root, a root of the computer or the user, or none. */
|
|
121
|
+
export const TRUSTS = ["public", "private", "none"] as const;
|
|
122
|
+
export type Trust = (typeof TRUSTS)[number];
|
|
123
|
+
|
|
124
|
+
/** The problems of a certificate. */
|
|
125
|
+
export const CERT_PROBLEMS = ["expired", "not-yet-valid", "name", "untrusted", "revoked", "other"] as const;
|
|
126
|
+
export type CertProblem = (typeof CERT_PROBLEMS)[number];
|
|
127
|
+
|
|
128
|
+
/** The certificate of a server. */
|
|
129
|
+
export interface TlsCertificate {
|
|
130
|
+
readonly subject: string;
|
|
131
|
+
readonly issuer: string;
|
|
132
|
+
/** The DNS names and the IP addresses of the certificate. */
|
|
133
|
+
readonly names: readonly string[];
|
|
134
|
+
/** The start of the validity, as an ISO 8601 time. */
|
|
135
|
+
readonly validFrom: string;
|
|
136
|
+
/** The end of the validity, as an ISO 8601 time. */
|
|
137
|
+
readonly validTo: string;
|
|
138
|
+
readonly fingerprint256: string;
|
|
139
|
+
readonly selfSigned: boolean;
|
|
140
|
+
/** The number of certificates from the certificate of the server to the root, with both. */
|
|
141
|
+
readonly chain: number;
|
|
142
|
+
/** The root that the chain ends at. A failed check of the certificate does not change it. */
|
|
143
|
+
readonly trust: Trust;
|
|
144
|
+
readonly problem?: CertProblem | undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The outcome of one TLS handshake. */
|
|
148
|
+
export interface TlsHandshake {
|
|
149
|
+
readonly address: string;
|
|
150
|
+
readonly port: number;
|
|
151
|
+
readonly servername?: string | undefined;
|
|
152
|
+
readonly outcome: HandshakeOutcome;
|
|
153
|
+
/** The time from the TCP connection to the end of the handshake or to the failure, in milliseconds. */
|
|
154
|
+
readonly ms: number;
|
|
155
|
+
/** The TLS version, for example `TLSv1.3`. */
|
|
156
|
+
readonly protocol?: string | undefined;
|
|
157
|
+
readonly cipher?: string | undefined;
|
|
158
|
+
/** The application protocol that the server chose with ALPN, for example `http/1.1`. */
|
|
159
|
+
readonly alpn?: string | undefined;
|
|
160
|
+
readonly code?: string | undefined;
|
|
161
|
+
readonly certificate?: TlsCertificate | undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isCertificate(value: unknown): value is TlsCertificate {
|
|
165
|
+
return (
|
|
166
|
+
isRecord(value) &&
|
|
167
|
+
typeof value["subject"] === "string" &&
|
|
168
|
+
typeof value["issuer"] === "string" &&
|
|
169
|
+
isTextList(value["names"]) &&
|
|
170
|
+
isText(value["validFrom"]) &&
|
|
171
|
+
isText(value["validTo"]) &&
|
|
172
|
+
isText(value["fingerprint256"]) &&
|
|
173
|
+
typeof value["selfSigned"] === "boolean" &&
|
|
174
|
+
typeof value["chain"] === "number" &&
|
|
175
|
+
Number.isInteger(value["chain"]) &&
|
|
176
|
+
value["chain"] >= 1 &&
|
|
177
|
+
isOneOf(TRUSTS, value["trust"]) &&
|
|
178
|
+
optional(value["problem"], (problem) => isOneOf(CERT_PROBLEMS, problem))
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The fact kind `tls.handshake`. */
|
|
183
|
+
export const TLS_HANDSHAKE: FactKind<TlsHandshake> = defineFactKind<TlsHandshake>({
|
|
184
|
+
kind: "tls.handshake",
|
|
185
|
+
layer: "tls",
|
|
186
|
+
summary: "The outcome and the time of one TLS handshake, and the certificate of the server.",
|
|
187
|
+
check: (value): value is TlsHandshake =>
|
|
188
|
+
isRecord(value) &&
|
|
189
|
+
isText(value["address"]) &&
|
|
190
|
+
isPort(value["port"]) &&
|
|
191
|
+
optional(value["servername"], isText) &&
|
|
192
|
+
isOneOf(HANDSHAKE_OUTCOMES, value["outcome"]) &&
|
|
193
|
+
isDuration(value["ms"]) &&
|
|
194
|
+
optional(value["protocol"], isText) &&
|
|
195
|
+
optional(value["cipher"], isText) &&
|
|
196
|
+
optional(value["alpn"], isText) &&
|
|
197
|
+
optional(value["code"], isText) &&
|
|
198
|
+
optional(value["certificate"], isCertificate),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The outcomes of an HTTP request.
|
|
203
|
+
*
|
|
204
|
+
* - `response`: the server sent a response. Its status can be an error.
|
|
205
|
+
* - `protocol`: the server sent bytes that are not HTTP.
|
|
206
|
+
* - `closed`: the server closed the connection before the response.
|
|
207
|
+
*/
|
|
208
|
+
export const HTTP_OUTCOMES = ["response", "protocol", "timeout", "reset", "closed", "error"] as const;
|
|
209
|
+
export type HttpOutcome = (typeof HTTP_OUTCOMES)[number];
|
|
210
|
+
|
|
211
|
+
/** The outcome of one HTTP request. */
|
|
212
|
+
export interface HttpResponse {
|
|
213
|
+
readonly address: string;
|
|
214
|
+
readonly port: number;
|
|
215
|
+
readonly scheme: "http" | "https";
|
|
216
|
+
readonly method: "GET" | "HEAD";
|
|
217
|
+
/** The value of the `Host` field of the request. */
|
|
218
|
+
readonly host: string;
|
|
219
|
+
readonly path: string;
|
|
220
|
+
readonly outcome: HttpOutcome;
|
|
221
|
+
readonly status?: number | undefined;
|
|
222
|
+
/** The HTTP version of the response, for example `1.1`. */
|
|
223
|
+
readonly version?: string | undefined;
|
|
224
|
+
/** Some fields of the response head, with lower-case names: `server`, `via`, `location` and others. */
|
|
225
|
+
readonly fields: Readonly<Record<string, string>>;
|
|
226
|
+
/** The bytes of the body that the probe read. */
|
|
227
|
+
readonly bytes: number;
|
|
228
|
+
/** The probe stopped the read of the body at its limit. */
|
|
229
|
+
readonly truncated: boolean;
|
|
230
|
+
/** The time from the request to the head of the response, in milliseconds. */
|
|
231
|
+
readonly ttfbMs?: number | undefined;
|
|
232
|
+
/** The time from the request to the end of the response or to the failure, in milliseconds. */
|
|
233
|
+
readonly ms: number;
|
|
234
|
+
readonly code?: string | undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The fact kind `http.response`. */
|
|
238
|
+
export const HTTP_RESPONSE: FactKind<HttpResponse> = defineFactKind<HttpResponse>({
|
|
239
|
+
kind: "http.response",
|
|
240
|
+
layer: "http",
|
|
241
|
+
summary: "The outcome and the times of one HTTP request: the status, some fields of the head and the time to the first byte.",
|
|
242
|
+
check: (value): value is HttpResponse =>
|
|
243
|
+
isRecord(value) &&
|
|
244
|
+
isText(value["address"]) &&
|
|
245
|
+
isPort(value["port"]) &&
|
|
246
|
+
(value["scheme"] === "http" || value["scheme"] === "https") &&
|
|
247
|
+
(value["method"] === "GET" || value["method"] === "HEAD") &&
|
|
248
|
+
isText(value["host"]) &&
|
|
249
|
+
isText(value["path"]) &&
|
|
250
|
+
isOneOf(HTTP_OUTCOMES, value["outcome"]) &&
|
|
251
|
+
(value["outcome"] !== "response" || (typeof value["status"] === "number" && Number.isInteger(value["status"]))) &&
|
|
252
|
+
optional(value["status"], (status) => typeof status === "number" && status >= 100 && status <= 999) &&
|
|
253
|
+
optional(value["version"], isText) &&
|
|
254
|
+
isRecord(value["fields"]) &&
|
|
255
|
+
Object.values(value["fields"]).every((field) => typeof field === "string") &&
|
|
256
|
+
typeof value["bytes"] === "number" &&
|
|
257
|
+
Number.isInteger(value["bytes"]) &&
|
|
258
|
+
value["bytes"] >= 0 &&
|
|
259
|
+
typeof value["truncated"] === "boolean" &&
|
|
260
|
+
optional(value["ttfbMs"], isDuration) &&
|
|
261
|
+
isDuration(value["ms"]) &&
|
|
262
|
+
optional(value["code"], isText),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
/** The fact kinds of the native probes. */
|
|
266
|
+
export const NATIVE_KINDS: readonly FactKind<unknown>[] = [DNS_ANSWER, TCP_CONNECT, TLS_HANDSHAKE, HTTP_RESPONSE];
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The outcomes of the ICMP echo requests to one address.
|
|
270
|
+
*
|
|
271
|
+
* - `reply`: one or more echo replies came back.
|
|
272
|
+
* - `none`: no reply came back in the time limit.
|
|
273
|
+
* - `unreachable`: no echo reply came back, and a router or this computer told that the address is unreachable.
|
|
274
|
+
* - `error`: the tool did not start, or netdiag cannot read its output.
|
|
275
|
+
*/
|
|
276
|
+
export const ECHO_OUTCOMES = ["reply", "none", "unreachable", "error"] as const;
|
|
277
|
+
export type EchoOutcome = (typeof ECHO_OUTCOMES)[number];
|
|
278
|
+
|
|
279
|
+
/** The result of the ICMP echo requests to one address. */
|
|
280
|
+
export interface IcmpEcho {
|
|
281
|
+
readonly address: string;
|
|
282
|
+
readonly outcome: EchoOutcome;
|
|
283
|
+
/** The number of echo requests that the tool sent. */
|
|
284
|
+
readonly sent: number;
|
|
285
|
+
/** The number of echo replies. A message of an unreachable address is not a reply. */
|
|
286
|
+
readonly received: number;
|
|
287
|
+
/** The round-trip time of each reply, in milliseconds. */
|
|
288
|
+
readonly rttMs: readonly number[];
|
|
289
|
+
/** The TTL (IPv4) or the hop limit (IPv6) of the first reply. */
|
|
290
|
+
readonly ttl?: number | undefined;
|
|
291
|
+
/** The router that told that the address is unreachable. */
|
|
292
|
+
readonly from?: string | undefined;
|
|
293
|
+
/** The text of the tool for the outcomes `unreachable` and `error`. */
|
|
294
|
+
readonly detail?: string | undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** The fact kind `icmp.echo`. */
|
|
298
|
+
export const ICMP_ECHO: FactKind<IcmpEcho> = defineFactKind<IcmpEcho>({
|
|
299
|
+
kind: "icmp.echo",
|
|
300
|
+
layer: "ip",
|
|
301
|
+
summary: "The replies to the ICMP echo requests to one address, their round-trip times and the messages of an unreachable address.",
|
|
302
|
+
check: (value): value is IcmpEcho =>
|
|
303
|
+
isRecord(value) &&
|
|
304
|
+
isText(value["address"]) &&
|
|
305
|
+
isOneOf(ECHO_OUTCOMES, value["outcome"]) &&
|
|
306
|
+
isCount(value["sent"]) &&
|
|
307
|
+
isCount(value["received"]) &&
|
|
308
|
+
value["received"] <= value["sent"] &&
|
|
309
|
+
Array.isArray(value["rttMs"]) &&
|
|
310
|
+
value["rttMs"].every(isDuration) &&
|
|
311
|
+
value["rttMs"].length <= value["received"] &&
|
|
312
|
+
(value["outcome"] === "reply") === value["received"] > 0 &&
|
|
313
|
+
optional(value["ttl"], isCount) &&
|
|
314
|
+
optional(value["from"], isText) &&
|
|
315
|
+
optional(value["detail"], isText),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The outcomes of a trace of the path to an address.
|
|
320
|
+
*
|
|
321
|
+
* - `reached`: the address answered at the last hop.
|
|
322
|
+
* - `stopped`: the last hop is not the address, and no router told that the address is unreachable.
|
|
323
|
+
* - `unreachable`: a router or this computer told that the address is unreachable.
|
|
324
|
+
* - `error`: the tool failed, or netdiag cannot read its output.
|
|
325
|
+
*/
|
|
326
|
+
export const TRACE_OUTCOMES = ["reached", "stopped", "unreachable", "error"] as const;
|
|
327
|
+
export type TraceOutcome = (typeof TRACE_OUTCOMES)[number];
|
|
328
|
+
|
|
329
|
+
/** One hop of a trace: the answers to the probes with one TTL. */
|
|
330
|
+
export interface TraceHop {
|
|
331
|
+
/** The TTL (IPv4) or the hop limit (IPv6) of the probes, from 1. */
|
|
332
|
+
readonly ttl: number;
|
|
333
|
+
/** The addresses that answered, in the order of their first answer. */
|
|
334
|
+
readonly addresses: readonly string[];
|
|
335
|
+
/** The number of probes that the tool sent with this TTL. */
|
|
336
|
+
readonly sent: number;
|
|
337
|
+
/** The number of answers. */
|
|
338
|
+
readonly received: number;
|
|
339
|
+
/** The round-trip time of each answer that has a time, in milliseconds. */
|
|
340
|
+
readonly rttMs: readonly number[];
|
|
341
|
+
/** The message of an unreachable address at this hop, for example `host unreachable (!H)`. */
|
|
342
|
+
readonly unreachable?: string | undefined;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The trace of the path to one address: the hops of the tool `traceroute`, or `tracert` on Windows. */
|
|
346
|
+
export interface PathTrace {
|
|
347
|
+
readonly address: string;
|
|
348
|
+
readonly outcome: TraceOutcome;
|
|
349
|
+
/** The largest TTL of the probes. */
|
|
350
|
+
readonly maxHops: number;
|
|
351
|
+
/** The hops in the order of their TTL. An `error` keeps the hops before the failure. */
|
|
352
|
+
readonly hops: readonly TraceHop[];
|
|
353
|
+
/** The text of the tool for the outcomes `unreachable` and `error`. */
|
|
354
|
+
readonly detail?: string | undefined;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function isTraceHop(value: unknown): value is TraceHop {
|
|
358
|
+
return (
|
|
359
|
+
isRecord(value) &&
|
|
360
|
+
isCount(value["ttl"]) &&
|
|
361
|
+
value["ttl"] >= 1 &&
|
|
362
|
+
isTextList(value["addresses"]) &&
|
|
363
|
+
isCount(value["sent"]) &&
|
|
364
|
+
isCount(value["received"]) &&
|
|
365
|
+
value["received"] <= value["sent"] &&
|
|
366
|
+
(value["received"] > 0) === (value["addresses"].length > 0) &&
|
|
367
|
+
Array.isArray(value["rttMs"]) &&
|
|
368
|
+
value["rttMs"].every(isDuration) &&
|
|
369
|
+
value["rttMs"].length <= value["received"] &&
|
|
370
|
+
optional(value["unreachable"], isText)
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** This function tells if the TTL of each hop is larger than the TTL of the hop before it, and not larger than `maxHops`. */
|
|
375
|
+
function inOrder(hops: readonly { readonly ttl: number }[], maxHops: number): boolean {
|
|
376
|
+
return hops.every((hop, index) => hop.ttl <= maxHops && (index === 0 || hop.ttl > (hops[index - 1]?.ttl ?? 0)));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The fact kind `path.trace`. */
|
|
380
|
+
export const PATH_TRACE: FactKind<PathTrace> = defineFactKind<PathTrace>({
|
|
381
|
+
kind: "path.trace",
|
|
382
|
+
layer: "path",
|
|
383
|
+
summary: "The hops of the path to one address: the routers that answered each TTL, their round-trip times and the messages of an unreachable address.",
|
|
384
|
+
check: (value): value is PathTrace =>
|
|
385
|
+
isRecord(value) &&
|
|
386
|
+
isText(value["address"]) &&
|
|
387
|
+
isOneOf(TRACE_OUTCOMES, value["outcome"]) &&
|
|
388
|
+
isCount(value["maxHops"]) &&
|
|
389
|
+
Array.isArray(value["hops"]) &&
|
|
390
|
+
value["hops"].every(isTraceHop) &&
|
|
391
|
+
inOrder(value["hops"], value["maxHops"]) &&
|
|
392
|
+
(value["outcome"] !== "reached" || value["hops"].length > 0) &&
|
|
393
|
+
optional(value["detail"], isText),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
/** One hop of a report of `mtr`: the counts and the times of many cycles of probes with one TTL. */
|
|
397
|
+
export interface ReportHop {
|
|
398
|
+
readonly ttl: number;
|
|
399
|
+
/** The address that answered. A hop with no answer has no address. */
|
|
400
|
+
readonly address?: string | undefined;
|
|
401
|
+
/** The number of probes that the tool sent with this TTL. */
|
|
402
|
+
readonly sent: number;
|
|
403
|
+
/** The number of answers. The adapter calculates it from the loss and the number of probes of the report. */
|
|
404
|
+
readonly received: number;
|
|
405
|
+
/** The best round-trip time, in milliseconds. Only a hop with answers has the times. */
|
|
406
|
+
readonly bestMs?: number | undefined;
|
|
407
|
+
/** The mean round-trip time, in milliseconds. */
|
|
408
|
+
readonly meanMs?: number | undefined;
|
|
409
|
+
/** The worst round-trip time, in milliseconds. */
|
|
410
|
+
readonly worstMs?: number | undefined;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** The report of the tool `mtr` for the path to one address. */
|
|
414
|
+
export interface PathReport {
|
|
415
|
+
readonly address: string;
|
|
416
|
+
/** The outcome. A report of `mtr` has no messages of an unreachable address, so it does not have the outcome `unreachable`. */
|
|
417
|
+
readonly outcome: TraceOutcome;
|
|
418
|
+
/** The number of cycles of probes. */
|
|
419
|
+
readonly cycles: number;
|
|
420
|
+
readonly hops: readonly ReportHop[];
|
|
421
|
+
/** The text of the tool for the outcome `error`. */
|
|
422
|
+
readonly detail?: string | undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function isReportHop(value: unknown): value is ReportHop {
|
|
426
|
+
return (
|
|
427
|
+
isRecord(value) &&
|
|
428
|
+
isCount(value["ttl"]) &&
|
|
429
|
+
value["ttl"] >= 1 &&
|
|
430
|
+
optional(value["address"], isText) &&
|
|
431
|
+
isCount(value["sent"]) &&
|
|
432
|
+
isCount(value["received"]) &&
|
|
433
|
+
value["received"] <= value["sent"] &&
|
|
434
|
+
(value["received"] === 0 || value["address"] !== undefined) &&
|
|
435
|
+
optional(value["bestMs"], isDuration) &&
|
|
436
|
+
optional(value["meanMs"], isDuration) &&
|
|
437
|
+
optional(value["worstMs"], isDuration)
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** The fact kind `path.report`. */
|
|
442
|
+
export const PATH_REPORT: FactKind<PathReport> = defineFactKind<PathReport>({
|
|
443
|
+
kind: "path.report",
|
|
444
|
+
layer: "path",
|
|
445
|
+
summary: "The hops of the path to one address over many cycles of probes: the address, the loss and the round-trip times of each hop.",
|
|
446
|
+
check: (value): value is PathReport =>
|
|
447
|
+
isRecord(value) &&
|
|
448
|
+
isText(value["address"]) &&
|
|
449
|
+
isOneOf(TRACE_OUTCOMES, value["outcome"]) &&
|
|
450
|
+
value["outcome"] !== "unreachable" &&
|
|
451
|
+
isCount(value["cycles"]) &&
|
|
452
|
+
Array.isArray(value["hops"]) &&
|
|
453
|
+
value["hops"].every(isReportHop) &&
|
|
454
|
+
inOrder(value["hops"], Number.MAX_SAFE_INTEGER) &&
|
|
455
|
+
(value["outcome"] !== "reached" || value["hops"].length > 0) &&
|
|
456
|
+
optional(value["detail"], isText),
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
/** The fact kinds of the adapters of outside tools (`@mark1russell7/netdiag/tools`). */
|
|
460
|
+
export const TOOL_KINDS: readonly FactKind<unknown>[] = [ICMP_ECHO, PATH_TRACE, PATH_REPORT];
|
package/src/layer.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The layers of the network stack that netdiag examines, from the bottom to the top.
|
|
3
|
+
*
|
|
4
|
+
* - `link`: the network interface and its physical link.
|
|
5
|
+
* - `wifi`: the Wi-Fi link: the signal, the channel and the rate.
|
|
6
|
+
* - `ip`: the addresses of the interfaces.
|
|
7
|
+
* - `route`: the routes of this computer and its gateway.
|
|
8
|
+
* - `path`: the hops through the provider networks to the remote host.
|
|
9
|
+
* - `dns`: the resolution of names to addresses.
|
|
10
|
+
* - `tcp`: the connections to a port.
|
|
11
|
+
* - `tls`: the handshake and the certificate of the server.
|
|
12
|
+
* - `http`: the request and the response.
|
|
13
|
+
*/
|
|
14
|
+
export const LAYERS = ["link", "wifi", "ip", "route", "path", "dns", "tcp", "tls", "http"] as const;
|
|
15
|
+
|
|
16
|
+
/** A layer of the network stack. The list `LAYERS` gives each layer in its order. */
|
|
17
|
+
export type Layer = (typeof LAYERS)[number];
|
|
18
|
+
|
|
19
|
+
const RANKS: ReadonlyMap<string, number> = new Map(LAYERS.map((layer, rank) => [layer, rank]));
|
|
20
|
+
|
|
21
|
+
/** This function tells if a value is the name of a layer. */
|
|
22
|
+
export function isLayer(value: unknown): value is Layer {
|
|
23
|
+
return typeof value === "string" && RANKS.has(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** This function gives the position of a layer in `LAYERS`. The bottom layer `link` has the position 0. */
|
|
27
|
+
export function layerRank(layer: Layer): number {
|
|
28
|
+
const rank = RANKS.get(layer);
|
|
29
|
+
if (rank === undefined) throw new RangeError(`"${String(layer)}" is not a layer`);
|
|
30
|
+
return rank;
|
|
31
|
+
}
|
package/src/probe.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Clock } from "@mark1russell7/algebra/clock";
|
|
2
|
+
import type { FactKind, Observation } from "./fact.ts";
|
|
3
|
+
import type { Layer } from "./layer.ts";
|
|
4
|
+
import type { Target } from "./target.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The mode of a probe.
|
|
8
|
+
*
|
|
9
|
+
* - `active`: the probe sends packets to its peers, for example a TCP connect.
|
|
10
|
+
* - `passive`: the probe reads only the state of this computer, for example its routes. It has no peers.
|
|
11
|
+
*/
|
|
12
|
+
export type ProbeMode = "active" | "passive";
|
|
13
|
+
|
|
14
|
+
/** The context of a run: the clock of the runner, and the signal that stops the run. */
|
|
15
|
+
export interface ProbeContext {
|
|
16
|
+
readonly clock: Clock;
|
|
17
|
+
readonly signal: AbortSignal;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The availability of a probe on this computer. An unavailable probe tells why, for example a tool that is not installed. */
|
|
21
|
+
export type Availability =
|
|
22
|
+
| { readonly available: true; readonly version?: string | undefined }
|
|
23
|
+
| { readonly available: false; readonly reason: string };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The contract of a probe. A probe is an adapter: it tells if it is available, it does its work, and it parses its raw output into
|
|
27
|
+
* observations.
|
|
28
|
+
*
|
|
29
|
+
* - `peers` gives each host that `run` sends packets to. A passive probe gives no peers. The runner examines each peer
|
|
30
|
+
* against the scope before `run` starts.
|
|
31
|
+
* - `run` does the effects and gives the raw output. A network failure is an observation, not an error. For example, a
|
|
32
|
+
* TCP connect that times out gives the outcome `timeout`. `run` throws only for a fault of the probe.
|
|
33
|
+
* - `parse` is a pure function of the raw output and the input. Thus a record of a raw output gives the same facts again,
|
|
34
|
+
* also after a correction of the parser.
|
|
35
|
+
*
|
|
36
|
+
* The input and the raw output are plain JSON data, so a record can keep them.
|
|
37
|
+
*/
|
|
38
|
+
export interface Probe<I, R> {
|
|
39
|
+
/** The id has lower-case words with dots, for example `tcp.connect`. */
|
|
40
|
+
readonly id: string;
|
|
41
|
+
/** The version of the probe changes when its facts change. */
|
|
42
|
+
readonly version: string;
|
|
43
|
+
readonly summary: string;
|
|
44
|
+
readonly layer: Layer;
|
|
45
|
+
readonly mode: ProbeMode;
|
|
46
|
+
/** The kinds of the facts that the probe gives. The runner refuses an observation of a different kind. */
|
|
47
|
+
readonly kinds: readonly FactKind<unknown>[];
|
|
48
|
+
/** The outside tool that the probe starts, for example `ping`. A native probe has no tool. */
|
|
49
|
+
readonly tool?: string | undefined;
|
|
50
|
+
peers(input: I): readonly Target[];
|
|
51
|
+
available(context: ProbeContext): Promise<Availability>;
|
|
52
|
+
run(input: I, context: ProbeContext): Promise<R>;
|
|
53
|
+
parse(raw: R, input: I): readonly Observation[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** This function makes a probe, and examines its fields. */
|
|
57
|
+
export function defineProbe<I, R>(probe: Probe<I, R>): Probe<I, R> {
|
|
58
|
+
if (!/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(probe.id)) throw new RangeError(`"${probe.id}" is not a valid probe id`);
|
|
59
|
+
if (probe.kinds.length === 0) throw new RangeError(`The probe "${probe.id}" gives no kind of fact`);
|
|
60
|
+
return probe;
|
|
61
|
+
}
|