@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/tools.js
ADDED
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
import { C as observe, d as PATH_REPORT, f as PATH_TRACE, k as target, l as ICMP_ECHO, y as defineProbe } from "./kinds-CQv769lK.js";
|
|
2
|
+
import { addressFamily, canonicalAddress } from "./address.js";
|
|
3
|
+
import { isIP } from "node:net";
|
|
4
|
+
// src/tools/host.ts
|
|
5
|
+
/** The time limit of the command that examines if a tool is available, in milliseconds. */
|
|
6
|
+
const AVAILABILITY_TIMEOUT_MS = 5e3;
|
|
7
|
+
/** The time that an adapter adds to the time limit of a command, in milliseconds. In this time, a tool writes its last lines. */
|
|
8
|
+
const TOOL_MARGIN_MS = 3e3;
|
|
9
|
+
/**
|
|
10
|
+
* This function gives the availability of a tool from the output of a harmless command. A tool is available when its
|
|
11
|
+
* program starts. The first group of `version` gives the version of the tool.
|
|
12
|
+
*/
|
|
13
|
+
function availabilityOf(tool, output, version) {
|
|
14
|
+
if (output.error !== void 0) return {
|
|
15
|
+
available: false,
|
|
16
|
+
reason: `The tool ${tool} does not start: ${output.error}`
|
|
17
|
+
};
|
|
18
|
+
const found = version?.exec(`${output.stdout}\n${output.stderr}`)?.[1];
|
|
19
|
+
return found === void 0 ? { available: true } : {
|
|
20
|
+
available: true,
|
|
21
|
+
version: found
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** This function starts a command on the host, and gives its raw output with the times of the clock of the runner. */
|
|
25
|
+
async function runTool(host, tool, context) {
|
|
26
|
+
const { command, args, timeoutMs, env } = tool;
|
|
27
|
+
const start = context.clock.now();
|
|
28
|
+
const output = await host.run(command, args, env === void 0 ? {
|
|
29
|
+
timeoutMs,
|
|
30
|
+
signal: context.signal
|
|
31
|
+
} : {
|
|
32
|
+
timeoutMs,
|
|
33
|
+
signal: context.signal,
|
|
34
|
+
env
|
|
35
|
+
});
|
|
36
|
+
const end = context.clock.now();
|
|
37
|
+
context.signal.throwIfAborted();
|
|
38
|
+
return {
|
|
39
|
+
platform: host.platform,
|
|
40
|
+
command,
|
|
41
|
+
args: [...args],
|
|
42
|
+
start,
|
|
43
|
+
end,
|
|
44
|
+
exitCode: output.exitCode,
|
|
45
|
+
stdout: output.stdout,
|
|
46
|
+
stderr: output.stderr,
|
|
47
|
+
...output.timedOut === true ? { timedOut: true } : {},
|
|
48
|
+
...output.error === void 0 ? {} : { error: output.error }
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// src/tools/licenses.ts
|
|
52
|
+
/**
|
|
53
|
+
* The licenses of the outside tools, as data. netdiag bundles no tool. A test makes sure that the probe of each adapter
|
|
54
|
+
* has a row with the use `adapter`.
|
|
55
|
+
*/
|
|
56
|
+
const TOOL_LICENSES = [
|
|
57
|
+
{
|
|
58
|
+
tool: "ping",
|
|
59
|
+
license: "The license of the operating system",
|
|
60
|
+
use: "adapter",
|
|
61
|
+
probe: "icmp.ping"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
tool: "traceroute",
|
|
65
|
+
license: "GPL on Linux",
|
|
66
|
+
use: "adapter",
|
|
67
|
+
probe: "path.traceroute"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
tool: "mtr",
|
|
71
|
+
license: "GPL",
|
|
72
|
+
use: "adapter",
|
|
73
|
+
probe: "path.mtr"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
tool: "tshark",
|
|
77
|
+
license: "GPL",
|
|
78
|
+
use: "adapter"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
tool: "iperf3",
|
|
82
|
+
license: "BSD",
|
|
83
|
+
use: "adapter"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
tool: "nmap",
|
|
87
|
+
license: "NPSL",
|
|
88
|
+
use: "not-bundled"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
tool: "masscan",
|
|
92
|
+
license: "AGPL",
|
|
93
|
+
use: "not-bundled"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
tool: "speedtest",
|
|
97
|
+
license: "Ookla EULA",
|
|
98
|
+
use: "not-bundled"
|
|
99
|
+
}
|
|
100
|
+
];
|
|
101
|
+
/** This function gives the row of a tool, or `undefined` for a tool that is not in the table. */
|
|
102
|
+
function toolLicense(tool) {
|
|
103
|
+
return TOOL_LICENSES.find((row) => row.tool === tool);
|
|
104
|
+
}
|
|
105
|
+
// src/tools/common.ts
|
|
106
|
+
/** The environment that makes the output of a tool English on Linux and macOS. */
|
|
107
|
+
const ENGLISH = { LC_ALL: "C" };
|
|
108
|
+
/** The end of a line of output, on each operating system. */
|
|
109
|
+
const LINES = /\r?\n/;
|
|
110
|
+
/** The messages of a local send that tell that the address is unreachable, on Linux and macOS. */
|
|
111
|
+
const LOCAL_UNREACHABLE = /\b(Network is unreachable|No route to host|Host is down|Network is down)\b/i;
|
|
112
|
+
/**
|
|
113
|
+
* This function gives the canonical form and the family of an IP address. It throws a `TypeError` for a name, because
|
|
114
|
+
* an adapter sends probes only to an address.
|
|
115
|
+
*/
|
|
116
|
+
function toolAddress(address) {
|
|
117
|
+
const family = addressFamily(address);
|
|
118
|
+
if (family === void 0) throw new TypeError(`"${address}" is not an IP address. Get the addresses of a name with the DNS probe first.`);
|
|
119
|
+
return {
|
|
120
|
+
address: canonicalAddress(address),
|
|
121
|
+
family
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** This function throws a `RangeError` when a value is not an integer from `min` to `max`. It gives the value. */
|
|
125
|
+
function integerIn(value, min, max, what) {
|
|
126
|
+
if (!Number.isInteger(value) || value < min || value > max) throw new RangeError(`${value} is not ${what} from ${min} to ${max}.`);
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
/** This function tells if two IP addresses are the same address. */
|
|
130
|
+
function sameAddress(a, b) {
|
|
131
|
+
return canonicalAddress(a) === canonicalAddress(b);
|
|
132
|
+
}
|
|
133
|
+
/** This function gives the first line of a text that is not empty, with no spaces at its ends. */
|
|
134
|
+
function firstLine(text) {
|
|
135
|
+
return text.split(LINES).find((line) => line.trim().length > 0)?.trim();
|
|
136
|
+
}
|
|
137
|
+
// src/tools/ping.ts
|
|
138
|
+
/** The default number of echo requests. The feature `path.loss` needs `LOSS_MIN_SENT` requests or more. */
|
|
139
|
+
const DEFAULT_PING_COUNT = 4;
|
|
140
|
+
/** The default time to wait for each reply, in milliseconds. */
|
|
141
|
+
const DEFAULT_PING_WAIT_MS = 1e3;
|
|
142
|
+
/** The largest number of echo requests in one input. */
|
|
143
|
+
const MAX_PING_COUNT = 100;
|
|
144
|
+
/** The time between two echo requests on Linux and macOS, in milliseconds. A user without privileges can set it. */
|
|
145
|
+
const PING_INTERVAL_MS = 200;
|
|
146
|
+
/**
|
|
147
|
+
* The time that the `ping6` of macOS waits after its last request when a reply is missing, in milliseconds. The tool
|
|
148
|
+
* has no option for it.
|
|
149
|
+
*/
|
|
150
|
+
const PING6_LAST_WAIT_MS = 1e4;
|
|
151
|
+
function checked$2(input) {
|
|
152
|
+
return {
|
|
153
|
+
...toolAddress(input.address),
|
|
154
|
+
count: integerIn(input.count ?? 4, 1, 100, "a number of requests"),
|
|
155
|
+
waitMs: integerIn(input.waitMs ?? 1e3, 1, 6e4, "a time in milliseconds")
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* This function gives the command of ping for an operating system and an input. For an operating system that the adapter
|
|
160
|
+
* does not know, it gives `undefined`. It throws for an input that is not valid.
|
|
161
|
+
*
|
|
162
|
+
* - Linux: iputils `ping`, with a deadline (`-w`) in seconds, so that ping writes its statistics.
|
|
163
|
+
* - macOS: `ping` with a deadline (`-t`), and `ping6` for an IPv6 address.
|
|
164
|
+
* - Windows: `ping.exe`. It sends one request each second.
|
|
165
|
+
*/
|
|
166
|
+
function pingCommand(platform, input) {
|
|
167
|
+
const { address, family, count, waitMs } = checked$2(input);
|
|
168
|
+
const interval = (count - 1) * 200;
|
|
169
|
+
const deadlineS = Math.ceil((interval + waitMs) / 1e3) + 1;
|
|
170
|
+
const seconds = (ms) => String(ms / 1e3);
|
|
171
|
+
switch (platform) {
|
|
172
|
+
case "linux": return {
|
|
173
|
+
command: "ping",
|
|
174
|
+
args: [
|
|
175
|
+
"-n",
|
|
176
|
+
"-c",
|
|
177
|
+
String(count),
|
|
178
|
+
"-i",
|
|
179
|
+
seconds(200),
|
|
180
|
+
"-W",
|
|
181
|
+
String(Math.ceil(waitMs / 1e3)),
|
|
182
|
+
"-w",
|
|
183
|
+
String(deadlineS),
|
|
184
|
+
address
|
|
185
|
+
],
|
|
186
|
+
timeoutMs: deadlineS * 1e3 + TOOL_MARGIN_MS,
|
|
187
|
+
env: ENGLISH
|
|
188
|
+
};
|
|
189
|
+
case "darwin": return family === "ipv4" ? {
|
|
190
|
+
command: "ping",
|
|
191
|
+
args: [
|
|
192
|
+
"-n",
|
|
193
|
+
"-c",
|
|
194
|
+
String(count),
|
|
195
|
+
"-i",
|
|
196
|
+
seconds(200),
|
|
197
|
+
"-W",
|
|
198
|
+
String(waitMs),
|
|
199
|
+
"-t",
|
|
200
|
+
String(deadlineS),
|
|
201
|
+
address
|
|
202
|
+
],
|
|
203
|
+
timeoutMs: deadlineS * 1e3 + TOOL_MARGIN_MS,
|
|
204
|
+
env: ENGLISH
|
|
205
|
+
} : {
|
|
206
|
+
command: "ping6",
|
|
207
|
+
args: [
|
|
208
|
+
"-n",
|
|
209
|
+
"-c",
|
|
210
|
+
String(count),
|
|
211
|
+
address
|
|
212
|
+
],
|
|
213
|
+
timeoutMs: (count - 1) * 1e3 + PING6_LAST_WAIT_MS + TOOL_MARGIN_MS,
|
|
214
|
+
env: ENGLISH
|
|
215
|
+
};
|
|
216
|
+
case "win32": return {
|
|
217
|
+
command: "ping",
|
|
218
|
+
args: [
|
|
219
|
+
"-n",
|
|
220
|
+
String(count),
|
|
221
|
+
"-w",
|
|
222
|
+
String(waitMs),
|
|
223
|
+
address
|
|
224
|
+
],
|
|
225
|
+
timeoutMs: count * Math.max(1e3, waitMs) + waitMs + TOOL_MARGIN_MS
|
|
226
|
+
};
|
|
227
|
+
default: return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** The command that examines if ping is available on each operating system, and the pattern of its version. */
|
|
231
|
+
const AVAILABILITY$1 = {
|
|
232
|
+
linux: {
|
|
233
|
+
args: ["-V"],
|
|
234
|
+
version: /iputils\D*(\d+)/i
|
|
235
|
+
},
|
|
236
|
+
darwin: { args: [] },
|
|
237
|
+
win32: { args: ["/?"] }
|
|
238
|
+
};
|
|
239
|
+
/** This function tells if ping is available on the host. On Linux, netdiag reads only the ping of iputils. */
|
|
240
|
+
async function pingAvailability(host, context) {
|
|
241
|
+
const check = AVAILABILITY$1[host.platform];
|
|
242
|
+
if (check === void 0) return {
|
|
243
|
+
available: false,
|
|
244
|
+
reason: `netdiag reads the output of ping only on Linux, macOS and Windows, not on ${host.platform}.`
|
|
245
|
+
};
|
|
246
|
+
const output = await host.run("ping", check.args, {
|
|
247
|
+
timeoutMs: AVAILABILITY_TIMEOUT_MS,
|
|
248
|
+
signal: context.signal,
|
|
249
|
+
env: ENGLISH
|
|
250
|
+
});
|
|
251
|
+
if (host.platform === "linux" && output.error === void 0 && !/iputils/i.test(`${output.stdout}\n${output.stderr}`)) return {
|
|
252
|
+
available: false,
|
|
253
|
+
reason: "netdiag reads only the ping of iputils on Linux. This ping does not tell an iputils version."
|
|
254
|
+
};
|
|
255
|
+
return availabilityOf("ping", output, check.version);
|
|
256
|
+
}
|
|
257
|
+
/** The messages of a failed send of ping on Linux and macOS, for example `socket: Operation not permitted`. */
|
|
258
|
+
const SEND_FAILURE$1 = /^(?:socket|bind|connect|sendmsg|sendto|send): \S/;
|
|
259
|
+
function readUnix$1(stdout, stderr) {
|
|
260
|
+
const replies = [];
|
|
261
|
+
let unreachable;
|
|
262
|
+
let failure;
|
|
263
|
+
for (const line of stdout.split(LINES)) {
|
|
264
|
+
const head = /^\d+ bytes from (\S+) (.*)$/.exec(line) ?? /^From (\S+) icmp_seq=\d+ (.*)$/.exec(line);
|
|
265
|
+
if (head === null) continue;
|
|
266
|
+
const from = withoutSeparator(head[1] ?? "");
|
|
267
|
+
const rest = head[2] ?? "";
|
|
268
|
+
const time = line.startsWith("From ") ? null : /\btime[=<]([\d.]+) ?ms/.exec(rest);
|
|
269
|
+
if (time !== null) {
|
|
270
|
+
if (rest.includes("DUP!")) continue;
|
|
271
|
+
const ttl = /\b(?:ttl|hlim)=(\d+)/.exec(rest)?.[1];
|
|
272
|
+
replies.push(ttl === void 0 ? { rttMs: Number(time[1]) } : {
|
|
273
|
+
rttMs: Number(time[1]),
|
|
274
|
+
ttl: Number(ttl)
|
|
275
|
+
});
|
|
276
|
+
} else if (/unreachable/i.test(rest) && unreachable === void 0) unreachable = {
|
|
277
|
+
from,
|
|
278
|
+
detail: rest.trim()
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
for (const line of stderr.split(LINES)) {
|
|
282
|
+
const text = line.replace(/^ping6?:\s*/, "").trim();
|
|
283
|
+
const local = LOCAL_UNREACHABLE.exec(text);
|
|
284
|
+
if (local !== null) unreachable ??= { detail: local[1] ?? text };
|
|
285
|
+
else if (SEND_FAILURE$1.test(text)) failure ??= text;
|
|
286
|
+
}
|
|
287
|
+
const sent = /^(\d+) packets transmitted, \d+ (?:packets )?received/m.exec(stdout)?.[1];
|
|
288
|
+
return {
|
|
289
|
+
sent: sent === void 0 ? void 0 : Number(sent),
|
|
290
|
+
replies,
|
|
291
|
+
unreachable,
|
|
292
|
+
failure
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/** This function removes the `:` or `,` after an address in a line of ping. An IPv6 address keeps its own last `:`. */
|
|
296
|
+
function withoutSeparator(text) {
|
|
297
|
+
return isIP(text) === 0 ? text.replace(/[:,]$/, "") : text;
|
|
298
|
+
}
|
|
299
|
+
function readWindows$1(stdout) {
|
|
300
|
+
const replies = [];
|
|
301
|
+
let unreachable;
|
|
302
|
+
let failure;
|
|
303
|
+
for (const line of stdout.split(LINES)) {
|
|
304
|
+
const reply = /^Reply from (\S+?): (?:bytes=\d+ )?time([=<])(\d+)ms(?: TTL=(\d+))?/.exec(line);
|
|
305
|
+
if (reply !== null) {
|
|
306
|
+
const ttl = reply[4];
|
|
307
|
+
replies.push(ttl === void 0 ? { rttMs: Number(reply[3]) } : {
|
|
308
|
+
rttMs: Number(reply[3]),
|
|
309
|
+
ttl: Number(ttl)
|
|
310
|
+
});
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const text = line.trim();
|
|
314
|
+
const message = /^(?:Reply from (\S+): )?(Destination .*)$/i.exec(text);
|
|
315
|
+
if (message !== null && unreachable === void 0 && /unreachable\.?$/i.test(message[2] ?? "")) {
|
|
316
|
+
const detail = (message[2] ?? "").replace(/\.$/, "");
|
|
317
|
+
unreachable = message[1] === void 0 ? { detail } : {
|
|
318
|
+
from: message[1],
|
|
319
|
+
detail
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (text.startsWith("PING: transmit failed. ")) failure ??= text.slice(6);
|
|
323
|
+
}
|
|
324
|
+
const sent = /Packets: Sent = (\d+), Received = \d+/.exec(stdout)?.[1];
|
|
325
|
+
return {
|
|
326
|
+
sent: sent === void 0 ? void 0 : Number(sent),
|
|
327
|
+
replies,
|
|
328
|
+
unreachable,
|
|
329
|
+
failure
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* This function reads the raw output of the ping probe into the value of a fact `icmp.echo`.
|
|
334
|
+
*
|
|
335
|
+
* - The replies are the lines of echo replies. A message of an unreachable address and a duplicate are not replies.
|
|
336
|
+
* - Output with no statistics gives the outcome `error`, also when it has replies. The time limit stopped such a ping.
|
|
337
|
+
*/
|
|
338
|
+
function readPing(raw, address) {
|
|
339
|
+
const error = (detail) => ({
|
|
340
|
+
address,
|
|
341
|
+
outcome: "error",
|
|
342
|
+
sent: 0,
|
|
343
|
+
received: 0,
|
|
344
|
+
rttMs: [],
|
|
345
|
+
detail
|
|
346
|
+
});
|
|
347
|
+
if (raw.error !== void 0) return error(raw.error);
|
|
348
|
+
const { sent, replies, unreachable, failure } = raw.platform === "win32" ? readWindows$1(raw.stdout) : readUnix$1(raw.stdout, raw.stderr);
|
|
349
|
+
const from = unreachable?.from === void 0 ? {} : { from: unreachable.from };
|
|
350
|
+
if (sent === void 0) {
|
|
351
|
+
if (unreachable !== void 0 && raw.timedOut !== true) return {
|
|
352
|
+
address,
|
|
353
|
+
outcome: "unreachable",
|
|
354
|
+
sent: 0,
|
|
355
|
+
received: 0,
|
|
356
|
+
rttMs: [],
|
|
357
|
+
...from,
|
|
358
|
+
detail: unreachable.detail
|
|
359
|
+
};
|
|
360
|
+
if (raw.timedOut === true) return error("The time limit stopped ping before its statistics.");
|
|
361
|
+
return error(failure ?? firstLine(raw.stderr) ?? "netdiag cannot read the output of ping. netdiag reads only English output.");
|
|
362
|
+
}
|
|
363
|
+
if (replies.length > sent) return error(`The output of ping has ${replies.length} replies to ${sent} requests.`);
|
|
364
|
+
const first = replies[0];
|
|
365
|
+
if (first !== void 0) return {
|
|
366
|
+
address,
|
|
367
|
+
outcome: "reply",
|
|
368
|
+
sent,
|
|
369
|
+
received: replies.length,
|
|
370
|
+
rttMs: replies.map((r) => r.rttMs),
|
|
371
|
+
...first.ttl === void 0 ? {} : { ttl: first.ttl }
|
|
372
|
+
};
|
|
373
|
+
if (unreachable !== void 0) return {
|
|
374
|
+
address,
|
|
375
|
+
outcome: "unreachable",
|
|
376
|
+
sent,
|
|
377
|
+
received: 0,
|
|
378
|
+
rttMs: [],
|
|
379
|
+
...from,
|
|
380
|
+
detail: unreachable.detail
|
|
381
|
+
};
|
|
382
|
+
if (failure !== void 0) return error(failure);
|
|
383
|
+
return {
|
|
384
|
+
address,
|
|
385
|
+
outcome: "none",
|
|
386
|
+
sent,
|
|
387
|
+
received: 0,
|
|
388
|
+
rttMs: []
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* This function makes the probe `icmp.ping`: the echo requests of the tool ping to one IP address. The host starts
|
|
393
|
+
* the tool. Give `runCommand` of `@procedures/shell`, or a fake host in a test.
|
|
394
|
+
*/
|
|
395
|
+
function pingProbe(host) {
|
|
396
|
+
return defineProbe({
|
|
397
|
+
id: "icmp.ping",
|
|
398
|
+
version: "1",
|
|
399
|
+
summary: "The ICMP echo requests of the tool ping to one IP address.",
|
|
400
|
+
layer: "ip",
|
|
401
|
+
mode: "active",
|
|
402
|
+
kinds: [ICMP_ECHO],
|
|
403
|
+
tool: "ping",
|
|
404
|
+
peers: (input) => [target(checked$2(input).address)],
|
|
405
|
+
available: (context) => pingAvailability(host, context),
|
|
406
|
+
async run(input, context) {
|
|
407
|
+
const command = pingCommand(host.platform, input);
|
|
408
|
+
if (command === void 0) throw new Error(`netdiag has no command of ping for ${host.platform}.`);
|
|
409
|
+
return runTool(host, command, context);
|
|
410
|
+
},
|
|
411
|
+
parse: (raw, input) => [observe(ICMP_ECHO, target(input.address), readPing(raw, input.address), {
|
|
412
|
+
start: raw.start,
|
|
413
|
+
end: raw.end
|
|
414
|
+
})]
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
// src/tools/traceroute.ts
|
|
418
|
+
/** The default largest TTL. */
|
|
419
|
+
const DEFAULT_TRACE_HOPS = 30;
|
|
420
|
+
/** The largest TTL of one input. */
|
|
421
|
+
const MAX_TRACE_HOPS = 64;
|
|
422
|
+
/** The default time to wait for each answer, in milliseconds. */
|
|
423
|
+
const DEFAULT_TRACE_WAIT_MS = 1e3;
|
|
424
|
+
/** The number of probes for each TTL. `tracert` on Windows always sends 3 probes. */
|
|
425
|
+
const TRACE_QUERIES = 3;
|
|
426
|
+
function checked$1(input) {
|
|
427
|
+
return {
|
|
428
|
+
...toolAddress(input.address),
|
|
429
|
+
maxHops: integerIn(input.maxHops ?? 30, 1, 64, "a number of hops"),
|
|
430
|
+
waitMs: integerIn(input.waitMs ?? 1e3, 1, 1e4, "a time in milliseconds")
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* This function gives the command of traceroute for an operating system and an input. For an operating system that the
|
|
435
|
+
* adapter does not know, it gives `undefined`. It throws for an input that is not valid.
|
|
436
|
+
*
|
|
437
|
+
* - Linux: `traceroute`, with UDP probes. A user without privileges can send them.
|
|
438
|
+
* - macOS: `traceroute`, and `traceroute6` for an IPv6 address.
|
|
439
|
+
* - Windows: `tracert.exe`, with ICMP echo requests.
|
|
440
|
+
*
|
|
441
|
+
* The time limit is the time of a trace that waits for each probe of each hop.
|
|
442
|
+
*/
|
|
443
|
+
function tracerouteCommand(platform, input) {
|
|
444
|
+
const { address, family, maxHops, waitMs } = checked$1(input);
|
|
445
|
+
const waitS = Math.ceil(waitMs / 1e3);
|
|
446
|
+
const unix = [
|
|
447
|
+
"-n",
|
|
448
|
+
"-q",
|
|
449
|
+
String(3),
|
|
450
|
+
"-w",
|
|
451
|
+
String(waitS),
|
|
452
|
+
"-m",
|
|
453
|
+
String(maxHops)
|
|
454
|
+
];
|
|
455
|
+
const unixTimeoutMs = maxHops * 3 * waitS * 1e3 + TOOL_MARGIN_MS;
|
|
456
|
+
switch (platform) {
|
|
457
|
+
case "linux": return {
|
|
458
|
+
command: "traceroute",
|
|
459
|
+
args: [
|
|
460
|
+
family === "ipv6" ? "-6" : "-4",
|
|
461
|
+
...unix,
|
|
462
|
+
address
|
|
463
|
+
],
|
|
464
|
+
timeoutMs: unixTimeoutMs,
|
|
465
|
+
env: ENGLISH
|
|
466
|
+
};
|
|
467
|
+
case "darwin": return {
|
|
468
|
+
command: family === "ipv6" ? "traceroute6" : "traceroute",
|
|
469
|
+
args: [...unix, address],
|
|
470
|
+
timeoutMs: unixTimeoutMs,
|
|
471
|
+
env: ENGLISH
|
|
472
|
+
};
|
|
473
|
+
case "win32": return {
|
|
474
|
+
command: "tracert",
|
|
475
|
+
args: [
|
|
476
|
+
"-d",
|
|
477
|
+
"-h",
|
|
478
|
+
String(maxHops),
|
|
479
|
+
"-w",
|
|
480
|
+
String(waitMs),
|
|
481
|
+
address
|
|
482
|
+
],
|
|
483
|
+
timeoutMs: maxHops * 3 * waitMs + TOOL_MARGIN_MS
|
|
484
|
+
};
|
|
485
|
+
default: return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/** The command that examines if traceroute is available on each operating system, and the pattern of its version. */
|
|
489
|
+
const AVAILABILITY = {
|
|
490
|
+
linux: {
|
|
491
|
+
command: "traceroute",
|
|
492
|
+
args: ["--version"],
|
|
493
|
+
version: /version (\d[\d.]*)/
|
|
494
|
+
},
|
|
495
|
+
darwin: {
|
|
496
|
+
command: "traceroute",
|
|
497
|
+
args: []
|
|
498
|
+
},
|
|
499
|
+
win32: {
|
|
500
|
+
command: "tracert",
|
|
501
|
+
args: ["/?"]
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
/** This function tells if traceroute is available on the host. On Linux, netdiag reads only the Modern traceroute for Linux. */
|
|
505
|
+
async function tracerouteAvailability(host, context) {
|
|
506
|
+
const check = AVAILABILITY[host.platform];
|
|
507
|
+
if (check === void 0) return {
|
|
508
|
+
available: false,
|
|
509
|
+
reason: `netdiag reads the output of traceroute only on Linux, macOS and Windows, not on ${host.platform}.`
|
|
510
|
+
};
|
|
511
|
+
const output = await host.run(check.command, check.args, {
|
|
512
|
+
timeoutMs: AVAILABILITY_TIMEOUT_MS,
|
|
513
|
+
signal: context.signal,
|
|
514
|
+
env: ENGLISH
|
|
515
|
+
});
|
|
516
|
+
if (host.platform === "linux" && output.error === void 0 && !/Modern traceroute/i.test(`${output.stdout}\n${output.stderr}`)) return {
|
|
517
|
+
available: false,
|
|
518
|
+
reason: "netdiag reads only the Modern traceroute for Linux (the package traceroute). This traceroute does not tell that name."
|
|
519
|
+
};
|
|
520
|
+
return availabilityOf(check.command, output, check.version);
|
|
521
|
+
}
|
|
522
|
+
/** The flags of the Linux and BSD traceroute after the time of an answer, and their meaning. */
|
|
523
|
+
const FLAGS = {
|
|
524
|
+
"!H": "host unreachable",
|
|
525
|
+
"!N": "network unreachable",
|
|
526
|
+
"!P": "protocol unreachable",
|
|
527
|
+
"!S": "source route failed",
|
|
528
|
+
"!F": "fragmentation needed",
|
|
529
|
+
"!X": "communication administratively prohibited",
|
|
530
|
+
"!A": "communication with the network administratively prohibited",
|
|
531
|
+
"!Z": "communication with the host administratively prohibited",
|
|
532
|
+
"!U": "destination network unknown",
|
|
533
|
+
"!W": "destination host unknown",
|
|
534
|
+
"!V": "host precedence violation",
|
|
535
|
+
"!C": "precedence cutoff"
|
|
536
|
+
};
|
|
537
|
+
/** This function gives the meaning of a flag, or `undefined` for the flag `!` alone. That flag tells only that the TTL of the answer is 1 or less. */
|
|
538
|
+
function flagText(flag) {
|
|
539
|
+
if (flag === "!") return void 0;
|
|
540
|
+
const code = /^!<?(\d+)>?$/.exec(flag)?.[1];
|
|
541
|
+
if (code !== void 0) return `unreachable, ICMP code ${code} (${flag})`;
|
|
542
|
+
return `${FLAGS[flag.replace(/^(!F)-?\d+$/, "$1")] ?? "unreachable"} (${flag})`;
|
|
543
|
+
}
|
|
544
|
+
/** One hop while the reader reads it. */
|
|
545
|
+
var HopBuilder = class {
|
|
546
|
+
addresses = [];
|
|
547
|
+
rttMs = [];
|
|
548
|
+
sent = 0;
|
|
549
|
+
received = 0;
|
|
550
|
+
unreachable;
|
|
551
|
+
/** This field is true when a time comes before an address. */
|
|
552
|
+
unread = false;
|
|
553
|
+
ttl;
|
|
554
|
+
current;
|
|
555
|
+
constructor(ttl) {
|
|
556
|
+
this.ttl = ttl;
|
|
557
|
+
}
|
|
558
|
+
address(text) {
|
|
559
|
+
this.current = text;
|
|
560
|
+
if (!this.addresses.some((known) => sameAddress(known, text))) this.addresses.push(text);
|
|
561
|
+
}
|
|
562
|
+
lost() {
|
|
563
|
+
this.sent += 1;
|
|
564
|
+
}
|
|
565
|
+
answer(rttMs) {
|
|
566
|
+
this.sent += 1;
|
|
567
|
+
if (this.current === void 0) {
|
|
568
|
+
this.unread = true;
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
this.received += 1;
|
|
572
|
+
if (rttMs !== void 0) this.rttMs.push(rttMs);
|
|
573
|
+
}
|
|
574
|
+
hop() {
|
|
575
|
+
const hop = {
|
|
576
|
+
ttl: this.ttl,
|
|
577
|
+
addresses: [...this.addresses],
|
|
578
|
+
sent: this.sent,
|
|
579
|
+
received: this.received,
|
|
580
|
+
rttMs: [...this.rttMs]
|
|
581
|
+
};
|
|
582
|
+
return this.unreachable === void 0 ? hop : {
|
|
583
|
+
...hop,
|
|
584
|
+
unreachable: this.unreachable
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
/** This function gives the address of a token, for example `192.0.2.1` or `(192.0.2.1)`, or `undefined`. */
|
|
589
|
+
function addressOf(token) {
|
|
590
|
+
const text = token.replace(/^\((.*)\)$/, "$1");
|
|
591
|
+
return isIP(text) === 0 ? void 0 : text;
|
|
592
|
+
}
|
|
593
|
+
/** The messages of traceroute on Linux and macOS when a socket operation fails, for example `socket: Operation not permitted`. */
|
|
594
|
+
const SEND_FAILURE = /^(?:socket|bind|connect|sendmsg|sendto|send): \S/;
|
|
595
|
+
function readUnix(stdout, stderr) {
|
|
596
|
+
const builders = [];
|
|
597
|
+
for (const line of stdout.split(LINES)) {
|
|
598
|
+
const numbered = /^\s*(\d+)\s(.*)$/.exec(line);
|
|
599
|
+
const continued = numbered === null && /^\s+\S/.test(line) ? builders.at(-1) : void 0;
|
|
600
|
+
if (numbered === null && continued === void 0) continue;
|
|
601
|
+
const builder = continued ?? new HopBuilder(Number(numbered?.[1]));
|
|
602
|
+
if (continued === void 0) builders.push(builder);
|
|
603
|
+
const tokens = (numbered?.[2] ?? line).trim().split(/\s+/);
|
|
604
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
605
|
+
const token = tokens[i] ?? "";
|
|
606
|
+
const address = addressOf(token);
|
|
607
|
+
const time = /^(\d+(?:\.\d+)?)(ms)?$/.exec(token);
|
|
608
|
+
if (token === "*") builder.lost();
|
|
609
|
+
else if (address !== void 0) builder.address(address);
|
|
610
|
+
else if (time !== null && (time[2] === "ms" || tokens[i + 1] === "ms")) {
|
|
611
|
+
builder.answer(Number(time[1]));
|
|
612
|
+
if (time[2] === void 0) i += 1;
|
|
613
|
+
} else if (token.startsWith("!")) builder.unreachable ??= flagText(token);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
let failure;
|
|
617
|
+
let local;
|
|
618
|
+
for (const line of stderr.split(LINES)) {
|
|
619
|
+
const text = line.replace(/^traceroute6?:\s*/, "").trim();
|
|
620
|
+
const unreachable = LOCAL_UNREACHABLE.exec(text);
|
|
621
|
+
if (unreachable !== null) local ??= unreachable[1] ?? text;
|
|
622
|
+
else if (SEND_FAILURE.test(text)) failure ??= text;
|
|
623
|
+
}
|
|
624
|
+
const header = /^traceroute6? to /m.test(`${stdout}\n${stderr}`);
|
|
625
|
+
return {
|
|
626
|
+
hops: builders.map((b) => b.hop()),
|
|
627
|
+
header,
|
|
628
|
+
complete: true,
|
|
629
|
+
unread: builders.find((b) => b.unread)?.ttl,
|
|
630
|
+
failure,
|
|
631
|
+
local
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
/** The text between the address of a router and its message in the output of `tracert`. */
|
|
635
|
+
const REPORTS = " reports: ";
|
|
636
|
+
function readWindows(stdout) {
|
|
637
|
+
const builders = [];
|
|
638
|
+
let failure;
|
|
639
|
+
for (const line of stdout.split(LINES)) {
|
|
640
|
+
const numbered = /^\s*(\d+)\s(.*)$/.exec(line);
|
|
641
|
+
if (numbered === null) continue;
|
|
642
|
+
const builder = new HopBuilder(Number(numbered[1]));
|
|
643
|
+
builders.push(builder);
|
|
644
|
+
const rest = (numbered[2] ?? "").trim();
|
|
645
|
+
const at = rest.indexOf(REPORTS);
|
|
646
|
+
const tokens = (at < 0 ? rest : rest.slice(0, at)).split(/\s+/).filter((token) => token.length > 0);
|
|
647
|
+
const address = tokens.map(addressOf).find((found) => found !== void 0);
|
|
648
|
+
if (address !== void 0) builder.address(address);
|
|
649
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
650
|
+
const token = tokens[i] ?? "";
|
|
651
|
+
const time = /^<?(\d+)$/.exec(token);
|
|
652
|
+
if (token === "*") builder.lost();
|
|
653
|
+
else if (time !== null && tokens[i + 1] === "ms") {
|
|
654
|
+
builder.answer(Math.max(1, Number(time[1])));
|
|
655
|
+
i += 1;
|
|
656
|
+
} else if (/^(General failure|Transmit error)/i.test(tokens.slice(i).join(" "))) {
|
|
657
|
+
failure ??= tokens.slice(i).join(" ").replace(/\.$/, "");
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (at >= 0) {
|
|
662
|
+
builder.answer(void 0);
|
|
663
|
+
builder.unreachable = rest.slice(at + 10).trim().replace(/\.$/, "");
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
hops: builders.map((b) => b.hop()),
|
|
668
|
+
header: /^Tracing route to /m.test(stdout),
|
|
669
|
+
complete: /^Trace complete\./m.test(stdout),
|
|
670
|
+
unread: builders.find((b) => b.unread)?.ttl,
|
|
671
|
+
failure
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* This function reads the raw output of the traceroute probe into the value of a fact `path.trace`.
|
|
676
|
+
*
|
|
677
|
+
* - The outcome is `reached` when the address answered at the last hop.
|
|
678
|
+
* - Output that netdiag cannot read gives the outcome `error`. An `error` keeps the hops that netdiag read.
|
|
679
|
+
*/
|
|
680
|
+
function readTraceroute(raw, input) {
|
|
681
|
+
const { address } = input;
|
|
682
|
+
const maxHops = input.maxHops ?? 30;
|
|
683
|
+
const result = (outcome, hops, detail) => detail === void 0 ? {
|
|
684
|
+
address,
|
|
685
|
+
outcome,
|
|
686
|
+
maxHops,
|
|
687
|
+
hops
|
|
688
|
+
} : {
|
|
689
|
+
address,
|
|
690
|
+
outcome,
|
|
691
|
+
maxHops,
|
|
692
|
+
hops,
|
|
693
|
+
detail
|
|
694
|
+
};
|
|
695
|
+
if (raw.error !== void 0) return result("error", [], raw.error);
|
|
696
|
+
const { hops, header, complete, unread, failure, local } = raw.platform === "win32" ? readWindows(raw.stdout) : readUnix(raw.stdout, raw.stderr);
|
|
697
|
+
if (unread !== void 0) return result("error", hops.filter((hop) => hop.ttl < unread), `netdiag cannot read the hop ${unread} of traceroute.`);
|
|
698
|
+
if (!header && local === void 0) return result("error", [], failure ?? firstLine(raw.stderr) ?? "netdiag cannot read the output of traceroute. netdiag reads only English output.");
|
|
699
|
+
if (raw.timedOut === true || !complete) return result("error", hops, "The time limit stopped traceroute before its end.");
|
|
700
|
+
const last = hops.at(-1);
|
|
701
|
+
if (last?.addresses.some((known) => sameAddress(known, address)) === true) return result("reached", hops);
|
|
702
|
+
if (last?.unreachable !== void 0) return result("unreachable", hops, last.unreachable);
|
|
703
|
+
if (local !== void 0) return result("unreachable", hops, local);
|
|
704
|
+
if (failure !== void 0) return result("error", hops, failure);
|
|
705
|
+
return result("stopped", hops);
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* This function makes the probe `path.traceroute`: the trace of the path to one IP address with the tool traceroute,
|
|
709
|
+
* or `tracert` on Windows. The host starts the tool.
|
|
710
|
+
*/
|
|
711
|
+
function tracerouteProbe(host) {
|
|
712
|
+
return defineProbe({
|
|
713
|
+
id: "path.traceroute",
|
|
714
|
+
version: "1",
|
|
715
|
+
summary: "The hops of the path to one IP address, from the tool traceroute or tracert.",
|
|
716
|
+
layer: "path",
|
|
717
|
+
mode: "active",
|
|
718
|
+
kinds: [PATH_TRACE],
|
|
719
|
+
tool: "traceroute",
|
|
720
|
+
peers: (input) => [target(checked$1(input).address)],
|
|
721
|
+
available: (context) => tracerouteAvailability(host, context),
|
|
722
|
+
async run(input, context) {
|
|
723
|
+
const command = tracerouteCommand(host.platform, input);
|
|
724
|
+
if (command === void 0) throw new Error(`netdiag has no command of traceroute for ${host.platform}.`);
|
|
725
|
+
return runTool(host, command, context);
|
|
726
|
+
},
|
|
727
|
+
parse: (raw, input) => [observe(PATH_TRACE, target(input.address), readTraceroute(raw, input), {
|
|
728
|
+
start: raw.start,
|
|
729
|
+
end: raw.end
|
|
730
|
+
})]
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
// src/tools/mtr.ts
|
|
734
|
+
/** The default number of cycles. It is `LOSS_MIN_SENT` of the readers, so a report gives the feature `path.loss`. */
|
|
735
|
+
const DEFAULT_MTR_CYCLES = 20;
|
|
736
|
+
/** The largest number of cycles of one input. */
|
|
737
|
+
const MAX_MTR_CYCLES = 100;
|
|
738
|
+
/** The time that mtr waits for the last answers after the last cycle, in milliseconds (its option `--gracetime`). */
|
|
739
|
+
const MTR_GRACE_MS = 5e3;
|
|
740
|
+
function checked(input) {
|
|
741
|
+
return {
|
|
742
|
+
...toolAddress(input.address),
|
|
743
|
+
cycles: integerIn(input.cycles ?? 20, 1, 100, "a number of cycles"),
|
|
744
|
+
maxHops: integerIn(input.maxHops ?? 30, 1, 64, "a number of hops")
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* This function gives the command of mtr for an operating system and an input. On Windows and on an operating system
|
|
749
|
+
* that the adapter does not know, it gives `undefined`. It throws for an input that is not valid.
|
|
750
|
+
*/
|
|
751
|
+
function mtrCommand(platform, input) {
|
|
752
|
+
const { address, family, cycles, maxHops } = checked(input);
|
|
753
|
+
if (platform !== "linux" && platform !== "darwin") return void 0;
|
|
754
|
+
return {
|
|
755
|
+
command: "mtr",
|
|
756
|
+
args: [
|
|
757
|
+
"--json",
|
|
758
|
+
"--no-dns",
|
|
759
|
+
family === "ipv6" ? "-6" : "-4",
|
|
760
|
+
"-c",
|
|
761
|
+
String(cycles),
|
|
762
|
+
"-m",
|
|
763
|
+
String(maxHops),
|
|
764
|
+
address
|
|
765
|
+
],
|
|
766
|
+
timeoutMs: cycles * 1e3 + MTR_GRACE_MS + TOOL_MARGIN_MS,
|
|
767
|
+
env: ENGLISH
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
/** This function tells if mtr is available on the host. The adapter knows Linux and macOS. */
|
|
771
|
+
async function mtrAvailability(host, context) {
|
|
772
|
+
if (host.platform !== "linux" && host.platform !== "darwin") return {
|
|
773
|
+
available: false,
|
|
774
|
+
reason: `netdiag starts mtr only on Linux and macOS, not on ${host.platform}.`
|
|
775
|
+
};
|
|
776
|
+
return availabilityOf("mtr", await host.run("mtr", ["--version"], {
|
|
777
|
+
timeoutMs: AVAILABILITY_TIMEOUT_MS,
|
|
778
|
+
signal: context.signal,
|
|
779
|
+
env: ENGLISH
|
|
780
|
+
}), /mtr (\d[\d.]*)/);
|
|
781
|
+
}
|
|
782
|
+
function finite(value) {
|
|
783
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
784
|
+
}
|
|
785
|
+
/** This function reads one hub of the JSON report, or gives `undefined` when netdiag cannot read it. */
|
|
786
|
+
function readHub(hub) {
|
|
787
|
+
if (typeof hub !== "object" || hub === null) return void 0;
|
|
788
|
+
const record = hub;
|
|
789
|
+
const ttl = Number(record["count"]);
|
|
790
|
+
const sent = record["Snt"];
|
|
791
|
+
const loss = finite(record["Loss%"]);
|
|
792
|
+
const host = record["host"];
|
|
793
|
+
if (!Number.isInteger(ttl) || ttl < 1 || typeof sent !== "number" || !Number.isInteger(sent) || sent < 0 || loss === void 0 || loss > 100) return void 0;
|
|
794
|
+
if (typeof host !== "string") return void 0;
|
|
795
|
+
const received = sent - Math.round(sent * loss / 100);
|
|
796
|
+
if (host === "???") return received === 0 ? {
|
|
797
|
+
ttl,
|
|
798
|
+
sent,
|
|
799
|
+
received
|
|
800
|
+
} : void 0;
|
|
801
|
+
if (received === 0) return {
|
|
802
|
+
ttl,
|
|
803
|
+
address: host,
|
|
804
|
+
sent,
|
|
805
|
+
received
|
|
806
|
+
};
|
|
807
|
+
const times = {
|
|
808
|
+
bestMs: finite(record["Best"]),
|
|
809
|
+
meanMs: finite(record["Avg"]),
|
|
810
|
+
worstMs: finite(record["Wrst"])
|
|
811
|
+
};
|
|
812
|
+
return {
|
|
813
|
+
ttl,
|
|
814
|
+
address: host,
|
|
815
|
+
sent,
|
|
816
|
+
received,
|
|
817
|
+
...times.bestMs === void 0 ? {} : { bestMs: times.bestMs },
|
|
818
|
+
...times.meanMs === void 0 ? {} : { meanMs: times.meanMs },
|
|
819
|
+
...times.worstMs === void 0 ? {} : { worstMs: times.worstMs }
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
/** This function gives the hubs of the JSON report of mtr, or `undefined` when the text is not such a report. */
|
|
823
|
+
function hubsOf(text) {
|
|
824
|
+
try {
|
|
825
|
+
const parsed = JSON.parse(text);
|
|
826
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
827
|
+
const report = parsed.report;
|
|
828
|
+
if (typeof report !== "object" || report === null) return void 0;
|
|
829
|
+
const hubs = report.hubs;
|
|
830
|
+
return Array.isArray(hubs) ? hubs : void 0;
|
|
831
|
+
} catch {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* This function reads the raw output of the mtr probe into the value of a fact `path.report`.
|
|
837
|
+
*
|
|
838
|
+
* - The number of answers of a hop comes from its loss and its number of probes.
|
|
839
|
+
* - Output that netdiag cannot read gives the outcome `error`.
|
|
840
|
+
*/
|
|
841
|
+
function readMtr(raw, input) {
|
|
842
|
+
const { address } = input;
|
|
843
|
+
const cycles = input.cycles ?? 20;
|
|
844
|
+
const error = (detail) => ({
|
|
845
|
+
address,
|
|
846
|
+
outcome: "error",
|
|
847
|
+
cycles,
|
|
848
|
+
hops: [],
|
|
849
|
+
detail
|
|
850
|
+
});
|
|
851
|
+
if (raw.error !== void 0) return error(raw.error);
|
|
852
|
+
if (raw.timedOut === true) return error("The time limit stopped mtr before its report.");
|
|
853
|
+
const hubs = hubsOf(raw.stdout);
|
|
854
|
+
if (hubs === void 0) return error(firstLine(raw.stderr) ?? "netdiag cannot read the JSON report of mtr.");
|
|
855
|
+
const hops = [];
|
|
856
|
+
for (const hub of hubs) {
|
|
857
|
+
const hop = readHub(hub);
|
|
858
|
+
if (hop === void 0) return error(`netdiag cannot read the hub ${hops.length + 1} of the report of mtr.`);
|
|
859
|
+
if (hop.ttl <= (hops.at(-1)?.ttl ?? 0)) return error(`The hubs of the report of mtr are not in the order of their TTL.`);
|
|
860
|
+
hops.push(hop);
|
|
861
|
+
}
|
|
862
|
+
const last = hops.at(-1);
|
|
863
|
+
if (last === void 0) return error(firstLine(raw.stderr) ?? "The report of mtr has no hub.");
|
|
864
|
+
return {
|
|
865
|
+
address,
|
|
866
|
+
outcome: last.address !== void 0 && last.received > 0 && sameAddress(last.address, address) ? "reached" : "stopped",
|
|
867
|
+
cycles,
|
|
868
|
+
hops
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
/** This function makes the probe `path.mtr`: many cycles of probes on the path to one IP address with the tool mtr. */
|
|
872
|
+
function mtrProbe(host) {
|
|
873
|
+
return defineProbe({
|
|
874
|
+
id: "path.mtr",
|
|
875
|
+
version: "1",
|
|
876
|
+
summary: "The loss and the round-trip times of each hop of the path to one IP address, from the tool mtr.",
|
|
877
|
+
layer: "path",
|
|
878
|
+
mode: "active",
|
|
879
|
+
kinds: [PATH_REPORT],
|
|
880
|
+
tool: "mtr",
|
|
881
|
+
peers: (input) => [target(checked(input).address)],
|
|
882
|
+
available: (context) => mtrAvailability(host, context),
|
|
883
|
+
async run(input, context) {
|
|
884
|
+
const command = mtrCommand(host.platform, input);
|
|
885
|
+
if (command === void 0) throw new Error(`netdiag has no command of mtr for ${host.platform}.`);
|
|
886
|
+
return runTool(host, command, context);
|
|
887
|
+
},
|
|
888
|
+
parse: (raw, input) => [observe(PATH_REPORT, target(input.address), readMtr(raw, input), {
|
|
889
|
+
start: raw.start,
|
|
890
|
+
end: raw.end
|
|
891
|
+
})]
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
export { AVAILABILITY_TIMEOUT_MS, DEFAULT_MTR_CYCLES, DEFAULT_PING_COUNT, DEFAULT_PING_WAIT_MS, DEFAULT_TRACE_HOPS, DEFAULT_TRACE_WAIT_MS, MAX_MTR_CYCLES, MAX_PING_COUNT, MAX_TRACE_HOPS, MTR_GRACE_MS, PING6_LAST_WAIT_MS, PING_INTERVAL_MS, TOOL_LICENSES, TOOL_MARGIN_MS, TRACE_QUERIES, availabilityOf, mtrAvailability, mtrCommand, mtrProbe, pingAvailability, pingCommand, pingProbe, readMtr, readPing, readTraceroute, runTool, toolLicense, tracerouteAvailability, tracerouteCommand, tracerouteProbe };
|
|
895
|
+
|
|
896
|
+
//# sourceMappingURL=tools.js.map
|