@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
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { observe, type Observation } from "../fact.ts";
|
|
3
|
+
import { ICMP_ECHO, type IcmpEcho } from "../kinds.ts";
|
|
4
|
+
import { defineProbe, type Availability, type Probe, type ProbeContext } from "../probe.ts";
|
|
5
|
+
import { target, type Target } from "../target.ts";
|
|
6
|
+
import { ENGLISH, firstLine, integerIn, LINES, LOCAL_UNREACHABLE, toolAddress, type ToolAddress } from "./common.ts";
|
|
7
|
+
import { AVAILABILITY_TIMEOUT_MS, availabilityOf, runTool, TOOL_MARGIN_MS, type ToolCommand, type ToolHost, type ToolRaw } from "./host.ts";
|
|
8
|
+
|
|
9
|
+
// The adapter of the tool `ping`. It reads the English output of iputils on Linux, of the BSD ping on macOS and of
|
|
10
|
+
// ping.exe on Windows. On Linux and macOS, `LC_ALL=C` makes the output English. On Windows, the language of the
|
|
11
|
+
// system sets the language of the output, so netdiag cannot read the output of a system in another language.
|
|
12
|
+
|
|
13
|
+
/** The input of the ping probe: echo requests to one IP address. */
|
|
14
|
+
export interface PingInput {
|
|
15
|
+
readonly address: string;
|
|
16
|
+
/** The number of echo requests. The default is `DEFAULT_PING_COUNT`. */
|
|
17
|
+
readonly count?: number | undefined;
|
|
18
|
+
/** The time to wait for each reply, in milliseconds. The default is `DEFAULT_PING_WAIT_MS`. */
|
|
19
|
+
readonly waitMs?: number | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The default number of echo requests. The feature `path.loss` needs `LOSS_MIN_SENT` requests or more. */
|
|
23
|
+
export const DEFAULT_PING_COUNT = 4;
|
|
24
|
+
|
|
25
|
+
/** The default time to wait for each reply, in milliseconds. */
|
|
26
|
+
export const DEFAULT_PING_WAIT_MS = 1000;
|
|
27
|
+
|
|
28
|
+
/** The largest number of echo requests in one input. */
|
|
29
|
+
export const MAX_PING_COUNT = 100;
|
|
30
|
+
|
|
31
|
+
/** The time between two echo requests on Linux and macOS, in milliseconds. A user without privileges can set it. */
|
|
32
|
+
export const PING_INTERVAL_MS = 200;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The time that the `ping6` of macOS waits after its last request when a reply is missing, in milliseconds. The tool
|
|
36
|
+
* has no option for it.
|
|
37
|
+
*/
|
|
38
|
+
export const PING6_LAST_WAIT_MS = 10_000;
|
|
39
|
+
|
|
40
|
+
interface CheckedInput extends ToolAddress {
|
|
41
|
+
readonly count: number;
|
|
42
|
+
readonly waitMs: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function checked(input: PingInput): CheckedInput {
|
|
46
|
+
return {
|
|
47
|
+
...toolAddress(input.address),
|
|
48
|
+
count: integerIn(input.count ?? DEFAULT_PING_COUNT, 1, MAX_PING_COUNT, "a number of requests"),
|
|
49
|
+
waitMs: integerIn(input.waitMs ?? DEFAULT_PING_WAIT_MS, 1, 60_000, "a time in milliseconds"),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* This function gives the command of ping for an operating system and an input. For an operating system that the adapter
|
|
55
|
+
* does not know, it gives `undefined`. It throws for an input that is not valid.
|
|
56
|
+
*
|
|
57
|
+
* - Linux: iputils `ping`, with a deadline (`-w`) in seconds, so that ping writes its statistics.
|
|
58
|
+
* - macOS: `ping` with a deadline (`-t`), and `ping6` for an IPv6 address.
|
|
59
|
+
* - Windows: `ping.exe`. It sends one request each second.
|
|
60
|
+
*/
|
|
61
|
+
export function pingCommand(platform: string, input: PingInput): ToolCommand | undefined {
|
|
62
|
+
const { address, family, count, waitMs } = checked(input);
|
|
63
|
+
const interval = (count - 1) * PING_INTERVAL_MS;
|
|
64
|
+
const deadlineS = Math.ceil((interval + waitMs) / 1000) + 1;
|
|
65
|
+
const seconds = (ms: number): string => String(ms / 1000);
|
|
66
|
+
switch (platform) {
|
|
67
|
+
case "linux":
|
|
68
|
+
return {
|
|
69
|
+
command: "ping",
|
|
70
|
+
args: ["-n", "-c", String(count), "-i", seconds(PING_INTERVAL_MS), "-W", String(Math.ceil(waitMs / 1000)), "-w", String(deadlineS), address],
|
|
71
|
+
timeoutMs: deadlineS * 1000 + TOOL_MARGIN_MS,
|
|
72
|
+
env: ENGLISH,
|
|
73
|
+
};
|
|
74
|
+
case "darwin":
|
|
75
|
+
return family === "ipv4"
|
|
76
|
+
? {
|
|
77
|
+
command: "ping",
|
|
78
|
+
args: ["-n", "-c", String(count), "-i", seconds(PING_INTERVAL_MS), "-W", String(waitMs), "-t", String(deadlineS), address],
|
|
79
|
+
timeoutMs: deadlineS * 1000 + TOOL_MARGIN_MS,
|
|
80
|
+
env: ENGLISH,
|
|
81
|
+
}
|
|
82
|
+
: { command: "ping6", args: ["-n", "-c", String(count), address], timeoutMs: (count - 1) * 1000 + PING6_LAST_WAIT_MS + TOOL_MARGIN_MS, env: ENGLISH };
|
|
83
|
+
case "win32":
|
|
84
|
+
return { command: "ping", args: ["-n", String(count), "-w", String(waitMs), address], timeoutMs: count * Math.max(1000, waitMs) + waitMs + TOOL_MARGIN_MS };
|
|
85
|
+
default:
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The command that examines if ping is available on each operating system, and the pattern of its version. */
|
|
91
|
+
const AVAILABILITY: Readonly<Record<string, { readonly args: readonly string[]; readonly version?: RegExp }>> = {
|
|
92
|
+
linux: { args: ["-V"], version: /iputils\D*(\d+)/i },
|
|
93
|
+
darwin: { args: [] },
|
|
94
|
+
win32: { args: ["/?"] },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** This function tells if ping is available on the host. On Linux, netdiag reads only the ping of iputils. */
|
|
98
|
+
export async function pingAvailability(host: ToolHost, context: ProbeContext): Promise<Availability> {
|
|
99
|
+
const check = AVAILABILITY[host.platform];
|
|
100
|
+
if (check === undefined) return { available: false, reason: `netdiag reads the output of ping only on Linux, macOS and Windows, not on ${host.platform}.` };
|
|
101
|
+
const output = await host.run("ping", check.args, { timeoutMs: AVAILABILITY_TIMEOUT_MS, signal: context.signal, env: ENGLISH });
|
|
102
|
+
if (host.platform === "linux" && output.error === undefined && !/iputils/i.test(`${output.stdout}\n${output.stderr}`)) {
|
|
103
|
+
return { available: false, reason: "netdiag reads only the ping of iputils on Linux. This ping does not tell an iputils version." };
|
|
104
|
+
}
|
|
105
|
+
return availabilityOf("ping", output, check.version);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The replies and the messages that netdiag reads in the output of ping. */
|
|
109
|
+
interface PingLines {
|
|
110
|
+
/** The number of requests from the statistics. Without statistics, netdiag cannot read the output. */
|
|
111
|
+
readonly sent?: number | undefined;
|
|
112
|
+
readonly replies: readonly { readonly rttMs: number; readonly ttl?: number | undefined }[];
|
|
113
|
+
readonly unreachable?: { readonly from?: string | undefined; readonly detail: string } | undefined;
|
|
114
|
+
/** A failure of this computer to send a request, for example a firewall rule. */
|
|
115
|
+
readonly failure?: string | undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The messages of a failed send of ping on Linux and macOS, for example `socket: Operation not permitted`. */
|
|
119
|
+
const SEND_FAILURE = /^(?:socket|bind|connect|sendmsg|sendto|send): \S/;
|
|
120
|
+
|
|
121
|
+
function readUnix(stdout: string, stderr: string): PingLines {
|
|
122
|
+
const replies: { rttMs: number; ttl?: number }[] = [];
|
|
123
|
+
let unreachable: PingLines["unreachable"];
|
|
124
|
+
let failure: string | undefined;
|
|
125
|
+
for (const line of stdout.split(LINES)) {
|
|
126
|
+
// Each pattern has one way to match, so a long line cannot make it slow. A second test examines the rest of the line.
|
|
127
|
+
// iputils: "From 192.0.2.5 icmp_seq=1 Destination Host Unreachable". macOS: "92 bytes from 192.0.2.5: Destination Host Unreachable".
|
|
128
|
+
const head = /^\d+ bytes from (\S+) (.*)$/.exec(line) ?? /^From (\S+) icmp_seq=\d+ (.*)$/.exec(line);
|
|
129
|
+
if (head === null) continue;
|
|
130
|
+
const from = withoutSeparator(head[1] ?? "");
|
|
131
|
+
const rest = head[2] ?? "";
|
|
132
|
+
const time = line.startsWith("From ") ? null : /\btime[=<]([\d.]+) ?ms/.exec(rest);
|
|
133
|
+
if (time !== null) {
|
|
134
|
+
if (rest.includes("DUP!")) continue;
|
|
135
|
+
const ttl = /\b(?:ttl|hlim)=(\d+)/.exec(rest)?.[1];
|
|
136
|
+
replies.push(ttl === undefined ? { rttMs: Number(time[1]) } : { rttMs: Number(time[1]), ttl: Number(ttl) });
|
|
137
|
+
} else if (/unreachable/i.test(rest) && unreachable === undefined) {
|
|
138
|
+
unreachable = { from, detail: rest.trim() };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// A warning on stderr is not a failure. Only a failed send is a failure.
|
|
142
|
+
for (const line of stderr.split(LINES)) {
|
|
143
|
+
const text = line.replace(/^ping6?:\s*/, "").trim();
|
|
144
|
+
const local = LOCAL_UNREACHABLE.exec(text);
|
|
145
|
+
if (local !== null) unreachable ??= { detail: local[1] ?? text };
|
|
146
|
+
else if (SEND_FAILURE.test(text)) failure ??= text;
|
|
147
|
+
}
|
|
148
|
+
const sent = /^(\d+) packets transmitted, \d+ (?:packets )?received/m.exec(stdout)?.[1];
|
|
149
|
+
return { sent: sent === undefined ? undefined : Number(sent), replies, unreachable, failure };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** This function removes the `:` or `,` after an address in a line of ping. An IPv6 address keeps its own last `:`. */
|
|
153
|
+
function withoutSeparator(text: string): string {
|
|
154
|
+
return isIP(text) === 0 ? text.replace(/[:,]$/, "") : text;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readWindows(stdout: string): PingLines {
|
|
158
|
+
const replies: { rttMs: number; ttl?: number }[] = [];
|
|
159
|
+
let unreachable: PingLines["unreachable"];
|
|
160
|
+
let failure: string | undefined;
|
|
161
|
+
for (const line of stdout.split(LINES)) {
|
|
162
|
+
// "Reply from 192.0.2.1: bytes=32 time=12ms TTL=118". IPv6 has no bytes and no TTL. "time<1ms" counts as 1 ms.
|
|
163
|
+
const reply = /^Reply from (\S+?): (?:bytes=\d+ )?time([=<])(\d+)ms(?: TTL=(\d+))?/.exec(line);
|
|
164
|
+
if (reply !== null) {
|
|
165
|
+
const ttl = reply[4];
|
|
166
|
+
replies.push(ttl === undefined ? { rttMs: Number(reply[3]) } : { rttMs: Number(reply[3]), ttl: Number(ttl) });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
// "Reply from 192.168.1.191: Destination host unreachable." or "Destination net unreachable."
|
|
170
|
+
const text = line.trim();
|
|
171
|
+
const message = /^(?:Reply from (\S+): )?(Destination .*)$/i.exec(text);
|
|
172
|
+
if (message !== null && unreachable === undefined && /unreachable\.?$/i.test(message[2] ?? "")) {
|
|
173
|
+
const detail = (message[2] ?? "").replace(/\.$/, "");
|
|
174
|
+
unreachable = message[1] === undefined ? { detail } : { from: message[1], detail };
|
|
175
|
+
}
|
|
176
|
+
if (text.startsWith("PING: transmit failed. ")) failure ??= text.slice("PING: ".length);
|
|
177
|
+
}
|
|
178
|
+
const sent = /Packets: Sent = (\d+), Received = \d+/.exec(stdout)?.[1];
|
|
179
|
+
return { sent: sent === undefined ? undefined : Number(sent), replies, unreachable, failure };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* This function reads the raw output of the ping probe into the value of a fact `icmp.echo`.
|
|
184
|
+
*
|
|
185
|
+
* - The replies are the lines of echo replies. A message of an unreachable address and a duplicate are not replies.
|
|
186
|
+
* - Output with no statistics gives the outcome `error`, also when it has replies. The time limit stopped such a ping.
|
|
187
|
+
*/
|
|
188
|
+
export function readPing(raw: ToolRaw, address: string): IcmpEcho {
|
|
189
|
+
const error = (detail: string): IcmpEcho => ({ address, outcome: "error", sent: 0, received: 0, rttMs: [], detail });
|
|
190
|
+
if (raw.error !== undefined) return error(raw.error);
|
|
191
|
+
const lines = raw.platform === "win32" ? readWindows(raw.stdout) : readUnix(raw.stdout, raw.stderr);
|
|
192
|
+
const { sent, replies, unreachable, failure } = lines;
|
|
193
|
+
const from = unreachable?.from === undefined ? {} : { from: unreachable.from };
|
|
194
|
+
if (sent === undefined) {
|
|
195
|
+
if (unreachable !== undefined && raw.timedOut !== true) return { address, outcome: "unreachable", sent: 0, received: 0, rttMs: [], ...from, detail: unreachable.detail };
|
|
196
|
+
if (raw.timedOut === true) return error("The time limit stopped ping before its statistics.");
|
|
197
|
+
return error(failure ?? firstLine(raw.stderr) ?? "netdiag cannot read the output of ping. netdiag reads only English output.");
|
|
198
|
+
}
|
|
199
|
+
if (replies.length > sent) return error(`The output of ping has ${replies.length} replies to ${sent} requests.`);
|
|
200
|
+
const first = replies[0];
|
|
201
|
+
if (first !== undefined) {
|
|
202
|
+
return { address, outcome: "reply", sent, received: replies.length, rttMs: replies.map((r) => r.rttMs), ...(first.ttl === undefined ? {} : { ttl: first.ttl }) };
|
|
203
|
+
}
|
|
204
|
+
if (unreachable !== undefined) return { address, outcome: "unreachable", sent, received: 0, rttMs: [], ...from, detail: unreachable.detail };
|
|
205
|
+
if (failure !== undefined) return error(failure);
|
|
206
|
+
return { address, outcome: "none", sent, received: 0, rttMs: [] };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* This function makes the probe `icmp.ping`: the echo requests of the tool ping to one IP address. The host starts
|
|
211
|
+
* the tool. Give `runCommand` of `@procedures/shell`, or a fake host in a test.
|
|
212
|
+
*/
|
|
213
|
+
export function pingProbe(host: ToolHost): Probe<PingInput, ToolRaw> {
|
|
214
|
+
return defineProbe<PingInput, ToolRaw>({
|
|
215
|
+
id: "icmp.ping",
|
|
216
|
+
version: "1",
|
|
217
|
+
summary: "The ICMP echo requests of the tool ping to one IP address.",
|
|
218
|
+
layer: "ip",
|
|
219
|
+
mode: "active",
|
|
220
|
+
kinds: [ICMP_ECHO],
|
|
221
|
+
tool: "ping",
|
|
222
|
+
peers: (input): readonly Target[] => [target(checked(input).address)],
|
|
223
|
+
available: (context) => pingAvailability(host, context),
|
|
224
|
+
async run(input, context) {
|
|
225
|
+
const command = pingCommand(host.platform, input);
|
|
226
|
+
if (command === undefined) throw new Error(`netdiag has no command of ping for ${host.platform}.`);
|
|
227
|
+
return runTool(host, command, context);
|
|
228
|
+
},
|
|
229
|
+
parse: (raw, input): readonly Observation[] => [observe(ICMP_ECHO, target(input.address), readPing(raw, input.address), { start: raw.start, end: raw.end })],
|
|
230
|
+
});
|
|
231
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { observe, type Observation } from "../fact.ts";
|
|
3
|
+
import { PATH_TRACE, type PathTrace, type TraceHop } from "../kinds.ts";
|
|
4
|
+
import { defineProbe, type Availability, type Probe, type ProbeContext } from "../probe.ts";
|
|
5
|
+
import { target, type Target } from "../target.ts";
|
|
6
|
+
import { ENGLISH, firstLine, integerIn, LINES, LOCAL_UNREACHABLE, sameAddress, toolAddress, type ToolAddress } from "./common.ts";
|
|
7
|
+
import { AVAILABILITY_TIMEOUT_MS, availabilityOf, runTool, TOOL_MARGIN_MS, type ToolCommand, type ToolHost, type ToolRaw } from "./host.ts";
|
|
8
|
+
|
|
9
|
+
// The adapter of the tool traceroute. It reads the English output of the Modern traceroute for Linux, of the BSD
|
|
10
|
+
// traceroute and traceroute6 on macOS, and of tracert.exe on Windows. Each tool sends `TRACE_QUERIES` probes for each TTL.
|
|
11
|
+
|
|
12
|
+
/** The input of the traceroute probe: the path to one IP address. */
|
|
13
|
+
export interface TraceInput {
|
|
14
|
+
readonly address: string;
|
|
15
|
+
/** The largest TTL. The default is `DEFAULT_TRACE_HOPS`. */
|
|
16
|
+
readonly maxHops?: number | undefined;
|
|
17
|
+
/** The time to wait for each answer, in milliseconds. On Linux and macOS, the tool rounds it up to full seconds. */
|
|
18
|
+
readonly waitMs?: number | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The default largest TTL. */
|
|
22
|
+
export const DEFAULT_TRACE_HOPS = 30;
|
|
23
|
+
|
|
24
|
+
/** The largest TTL of one input. */
|
|
25
|
+
export const MAX_TRACE_HOPS = 64;
|
|
26
|
+
|
|
27
|
+
/** The default time to wait for each answer, in milliseconds. */
|
|
28
|
+
export const DEFAULT_TRACE_WAIT_MS = 1000;
|
|
29
|
+
|
|
30
|
+
/** The number of probes for each TTL. `tracert` on Windows always sends 3 probes. */
|
|
31
|
+
export const TRACE_QUERIES = 3;
|
|
32
|
+
|
|
33
|
+
interface CheckedInput extends ToolAddress {
|
|
34
|
+
readonly maxHops: number;
|
|
35
|
+
readonly waitMs: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function checked(input: TraceInput): CheckedInput {
|
|
39
|
+
return {
|
|
40
|
+
...toolAddress(input.address),
|
|
41
|
+
maxHops: integerIn(input.maxHops ?? DEFAULT_TRACE_HOPS, 1, MAX_TRACE_HOPS, "a number of hops"),
|
|
42
|
+
waitMs: integerIn(input.waitMs ?? DEFAULT_TRACE_WAIT_MS, 1, 10_000, "a time in milliseconds"),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* This function gives the command of traceroute for an operating system and an input. For an operating system that the
|
|
48
|
+
* adapter does not know, it gives `undefined`. It throws for an input that is not valid.
|
|
49
|
+
*
|
|
50
|
+
* - Linux: `traceroute`, with UDP probes. A user without privileges can send them.
|
|
51
|
+
* - macOS: `traceroute`, and `traceroute6` for an IPv6 address.
|
|
52
|
+
* - Windows: `tracert.exe`, with ICMP echo requests.
|
|
53
|
+
*
|
|
54
|
+
* The time limit is the time of a trace that waits for each probe of each hop.
|
|
55
|
+
*/
|
|
56
|
+
export function tracerouteCommand(platform: string, input: TraceInput): ToolCommand | undefined {
|
|
57
|
+
const { address, family, maxHops, waitMs } = checked(input);
|
|
58
|
+
const waitS = Math.ceil(waitMs / 1000);
|
|
59
|
+
const unix = ["-n", "-q", String(TRACE_QUERIES), "-w", String(waitS), "-m", String(maxHops)];
|
|
60
|
+
const unixTimeoutMs = maxHops * TRACE_QUERIES * waitS * 1000 + TOOL_MARGIN_MS;
|
|
61
|
+
switch (platform) {
|
|
62
|
+
case "linux":
|
|
63
|
+
return { command: "traceroute", args: [family === "ipv6" ? "-6" : "-4", ...unix, address], timeoutMs: unixTimeoutMs, env: ENGLISH };
|
|
64
|
+
case "darwin":
|
|
65
|
+
return { command: family === "ipv6" ? "traceroute6" : "traceroute", args: [...unix, address], timeoutMs: unixTimeoutMs, env: ENGLISH };
|
|
66
|
+
case "win32":
|
|
67
|
+
return { command: "tracert", args: ["-d", "-h", String(maxHops), "-w", String(waitMs), address], timeoutMs: maxHops * TRACE_QUERIES * waitMs + TOOL_MARGIN_MS };
|
|
68
|
+
default:
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The command that examines if traceroute is available on each operating system, and the pattern of its version. */
|
|
74
|
+
const AVAILABILITY: Readonly<Record<string, { readonly command: string; readonly args: readonly string[]; readonly version?: RegExp }>> = {
|
|
75
|
+
linux: { command: "traceroute", args: ["--version"], version: /version (\d[\d.]*)/ },
|
|
76
|
+
darwin: { command: "traceroute", args: [] },
|
|
77
|
+
win32: { command: "tracert", args: ["/?"] },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** This function tells if traceroute is available on the host. On Linux, netdiag reads only the Modern traceroute for Linux. */
|
|
81
|
+
export async function tracerouteAvailability(host: ToolHost, context: ProbeContext): Promise<Availability> {
|
|
82
|
+
const check = AVAILABILITY[host.platform];
|
|
83
|
+
if (check === undefined) return { available: false, reason: `netdiag reads the output of traceroute only on Linux, macOS and Windows, not on ${host.platform}.` };
|
|
84
|
+
const output = await host.run(check.command, check.args, { timeoutMs: AVAILABILITY_TIMEOUT_MS, signal: context.signal, env: ENGLISH });
|
|
85
|
+
if (host.platform === "linux" && output.error === undefined && !/Modern traceroute/i.test(`${output.stdout}\n${output.stderr}`)) {
|
|
86
|
+
return { available: false, reason: "netdiag reads only the Modern traceroute for Linux (the package traceroute). This traceroute does not tell that name." };
|
|
87
|
+
}
|
|
88
|
+
return availabilityOf(check.command, output, check.version);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The flags of the Linux and BSD traceroute after the time of an answer, and their meaning. */
|
|
92
|
+
const FLAGS: Readonly<Record<string, string>> = {
|
|
93
|
+
"!H": "host unreachable",
|
|
94
|
+
"!N": "network unreachable",
|
|
95
|
+
"!P": "protocol unreachable",
|
|
96
|
+
"!S": "source route failed",
|
|
97
|
+
"!F": "fragmentation needed",
|
|
98
|
+
"!X": "communication administratively prohibited",
|
|
99
|
+
"!A": "communication with the network administratively prohibited",
|
|
100
|
+
"!Z": "communication with the host administratively prohibited",
|
|
101
|
+
"!U": "destination network unknown",
|
|
102
|
+
"!W": "destination host unknown",
|
|
103
|
+
"!V": "host precedence violation",
|
|
104
|
+
"!C": "precedence cutoff",
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** 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. */
|
|
108
|
+
function flagText(flag: string): string | undefined {
|
|
109
|
+
if (flag === "!") return undefined;
|
|
110
|
+
const code = /^!<?(\d+)>?$/.exec(flag)?.[1];
|
|
111
|
+
if (code !== undefined) return `unreachable, ICMP code ${code} (${flag})`;
|
|
112
|
+
const known = FLAGS[flag.replace(/^(!F)-?\d+$/, "$1")];
|
|
113
|
+
return `${known ?? "unreachable"} (${flag})`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The hops that netdiag reads in the output of a trace, and the facts about the output. */
|
|
117
|
+
interface TraceLines {
|
|
118
|
+
readonly hops: readonly TraceHop[];
|
|
119
|
+
/** This field is true when the output has the header of the tool. Output in another language has no such header. */
|
|
120
|
+
readonly header: boolean;
|
|
121
|
+
/** This field is true when the output has the last line of a trace (`tracert` only). */
|
|
122
|
+
readonly complete: boolean;
|
|
123
|
+
/** The first hop that netdiag cannot read. */
|
|
124
|
+
readonly unread?: number | undefined;
|
|
125
|
+
/** A failure of this computer to send a probe. */
|
|
126
|
+
readonly failure?: string | undefined;
|
|
127
|
+
/** A message of this computer that tells that the address is unreachable. */
|
|
128
|
+
readonly local?: string | undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** One hop while the reader reads it. */
|
|
132
|
+
class HopBuilder {
|
|
133
|
+
readonly addresses: string[] = [];
|
|
134
|
+
readonly rttMs: number[] = [];
|
|
135
|
+
sent = 0;
|
|
136
|
+
received = 0;
|
|
137
|
+
unreachable: string | undefined;
|
|
138
|
+
/** This field is true when a time comes before an address. */
|
|
139
|
+
unread = false;
|
|
140
|
+
readonly ttl: number;
|
|
141
|
+
private current: string | undefined;
|
|
142
|
+
|
|
143
|
+
constructor(ttl: number) {
|
|
144
|
+
this.ttl = ttl;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
address(text: string): void {
|
|
148
|
+
this.current = text;
|
|
149
|
+
if (!this.addresses.some((known) => sameAddress(known, text))) this.addresses.push(text);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
lost(): void {
|
|
153
|
+
this.sent += 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
answer(rttMs: number | undefined): void {
|
|
157
|
+
this.sent += 1;
|
|
158
|
+
if (this.current === undefined) {
|
|
159
|
+
this.unread = true;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.received += 1;
|
|
163
|
+
if (rttMs !== undefined) this.rttMs.push(rttMs);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
hop(): TraceHop {
|
|
167
|
+
const hop = { ttl: this.ttl, addresses: [...this.addresses], sent: this.sent, received: this.received, rttMs: [...this.rttMs] };
|
|
168
|
+
return this.unreachable === undefined ? hop : { ...hop, unreachable: this.unreachable };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** This function gives the address of a token, for example `192.0.2.1` or `(192.0.2.1)`, or `undefined`. */
|
|
173
|
+
function addressOf(token: string): string | undefined {
|
|
174
|
+
const text = token.replace(/^\((.*)\)$/, "$1");
|
|
175
|
+
return isIP(text) === 0 ? undefined : text;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The messages of traceroute on Linux and macOS when a socket operation fails, for example `socket: Operation not permitted`. */
|
|
179
|
+
const SEND_FAILURE = /^(?:socket|bind|connect|sendmsg|sendto|send): \S/;
|
|
180
|
+
|
|
181
|
+
function readUnix(stdout: string, stderr: string): TraceLines {
|
|
182
|
+
const builders: HopBuilder[] = [];
|
|
183
|
+
for (const line of stdout.split(LINES)) {
|
|
184
|
+
const numbered = /^\s*(\d+)\s(.*)$/.exec(line);
|
|
185
|
+
// macOS writes another address of the same TTL on a line with no number.
|
|
186
|
+
const continued = numbered === null && /^\s+\S/.test(line) ? builders.at(-1) : undefined;
|
|
187
|
+
if (numbered === null && continued === undefined) continue;
|
|
188
|
+
const builder = continued ?? new HopBuilder(Number(numbered?.[1]));
|
|
189
|
+
if (continued === undefined) builders.push(builder);
|
|
190
|
+
const tokens = (numbered?.[2] ?? line).trim().split(/\s+/);
|
|
191
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
192
|
+
const token = tokens[i] ?? "";
|
|
193
|
+
const address = addressOf(token);
|
|
194
|
+
const time = /^(\d+(?:\.\d+)?)(ms)?$/.exec(token);
|
|
195
|
+
if (token === "*") builder.lost();
|
|
196
|
+
else if (address !== undefined) builder.address(address);
|
|
197
|
+
else if (time !== null && (time[2] === "ms" || tokens[i + 1] === "ms")) {
|
|
198
|
+
builder.answer(Number(time[1]));
|
|
199
|
+
if (time[2] === undefined) i += 1;
|
|
200
|
+
} else if (token.startsWith("!")) builder.unreachable ??= flagText(token);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
let failure: string | undefined;
|
|
204
|
+
let local: string | undefined;
|
|
205
|
+
for (const line of stderr.split(LINES)) {
|
|
206
|
+
const text = line.replace(/^traceroute6?:\s*/, "").trim();
|
|
207
|
+
const unreachable = LOCAL_UNREACHABLE.exec(text);
|
|
208
|
+
if (unreachable !== null) local ??= unreachable[1] ?? text;
|
|
209
|
+
else if (SEND_FAILURE.test(text)) failure ??= text;
|
|
210
|
+
}
|
|
211
|
+
const header = /^traceroute6? to /m.test(`${stdout}\n${stderr}`);
|
|
212
|
+
return { hops: builders.map((b) => b.hop()), header, complete: true, unread: builders.find((b) => b.unread)?.ttl, failure, local };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The text between the address of a router and its message in the output of `tracert`. */
|
|
216
|
+
const REPORTS = " reports: ";
|
|
217
|
+
|
|
218
|
+
function readWindows(stdout: string): TraceLines {
|
|
219
|
+
const builders: HopBuilder[] = [];
|
|
220
|
+
let failure: string | undefined;
|
|
221
|
+
for (const line of stdout.split(LINES)) {
|
|
222
|
+
const numbered = /^\s*(\d+)\s(.*)$/.exec(line);
|
|
223
|
+
if (numbered === null) continue;
|
|
224
|
+
const builder = new HopBuilder(Number(numbered[1]));
|
|
225
|
+
builders.push(builder);
|
|
226
|
+
// " 2 * * 192.168.1.191 reports: Destination host unreachable."
|
|
227
|
+
const rest = (numbered[2] ?? "").trim();
|
|
228
|
+
const at = rest.indexOf(REPORTS);
|
|
229
|
+
const tokens = (at < 0 ? rest : rest.slice(0, at)).split(/\s+/).filter((token) => token.length > 0);
|
|
230
|
+
// tracert writes the address after the times of the hop.
|
|
231
|
+
const address = tokens.map(addressOf).find((found) => found !== undefined);
|
|
232
|
+
if (address !== undefined) builder.address(address);
|
|
233
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
234
|
+
const token = tokens[i] ?? "";
|
|
235
|
+
// "<1 ms" counts as 1 ms, as in the output of ping.
|
|
236
|
+
const time = /^<?(\d+)$/.exec(token);
|
|
237
|
+
if (token === "*") builder.lost();
|
|
238
|
+
else if (time !== null && tokens[i + 1] === "ms") {
|
|
239
|
+
builder.answer(Math.max(1, Number(time[1])));
|
|
240
|
+
i += 1;
|
|
241
|
+
} else if (/^(General failure|Transmit error)/i.test(tokens.slice(i).join(" "))) {
|
|
242
|
+
failure ??= tokens.slice(i).join(" ").replace(/\.$/, "");
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (at >= 0) {
|
|
247
|
+
builder.answer(undefined);
|
|
248
|
+
builder.unreachable = rest
|
|
249
|
+
.slice(at + REPORTS.length)
|
|
250
|
+
.trim()
|
|
251
|
+
.replace(/\.$/, "");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
hops: builders.map((b) => b.hop()),
|
|
256
|
+
header: /^Tracing route to /m.test(stdout),
|
|
257
|
+
complete: /^Trace complete\./m.test(stdout),
|
|
258
|
+
unread: builders.find((b) => b.unread)?.ttl,
|
|
259
|
+
failure,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* This function reads the raw output of the traceroute probe into the value of a fact `path.trace`.
|
|
265
|
+
*
|
|
266
|
+
* - The outcome is `reached` when the address answered at the last hop.
|
|
267
|
+
* - Output that netdiag cannot read gives the outcome `error`. An `error` keeps the hops that netdiag read.
|
|
268
|
+
*/
|
|
269
|
+
export function readTraceroute(raw: ToolRaw, input: { readonly address: string; readonly maxHops?: number | undefined }): PathTrace {
|
|
270
|
+
const { address } = input;
|
|
271
|
+
const maxHops = input.maxHops ?? DEFAULT_TRACE_HOPS;
|
|
272
|
+
const result = (outcome: PathTrace["outcome"], hops: readonly TraceHop[], detail?: string): PathTrace =>
|
|
273
|
+
detail === undefined ? { address, outcome, maxHops, hops } : { address, outcome, maxHops, hops, detail };
|
|
274
|
+
if (raw.error !== undefined) return result("error", [], raw.error);
|
|
275
|
+
const lines = raw.platform === "win32" ? readWindows(raw.stdout) : readUnix(raw.stdout, raw.stderr);
|
|
276
|
+
const { hops, header, complete, unread, failure, local } = lines;
|
|
277
|
+
if (unread !== undefined) return result("error", hops.filter((hop) => hop.ttl < unread), `netdiag cannot read the hop ${unread} of traceroute.`);
|
|
278
|
+
// Output in another language has numbered lines too, but no English header.
|
|
279
|
+
if (!header && local === undefined) {
|
|
280
|
+
return result("error", [], failure ?? firstLine(raw.stderr) ?? "netdiag cannot read the output of traceroute. netdiag reads only English output.");
|
|
281
|
+
}
|
|
282
|
+
if (raw.timedOut === true || !complete) return result("error", hops, "The time limit stopped traceroute before its end.");
|
|
283
|
+
const last = hops.at(-1);
|
|
284
|
+
if (last?.addresses.some((known) => sameAddress(known, address)) === true) return result("reached", hops);
|
|
285
|
+
if (last?.unreachable !== undefined) return result("unreachable", hops, last.unreachable);
|
|
286
|
+
if (local !== undefined) return result("unreachable", hops, local);
|
|
287
|
+
if (failure !== undefined) return result("error", hops, failure);
|
|
288
|
+
return result("stopped", hops);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* This function makes the probe `path.traceroute`: the trace of the path to one IP address with the tool traceroute,
|
|
293
|
+
* or `tracert` on Windows. The host starts the tool.
|
|
294
|
+
*/
|
|
295
|
+
export function tracerouteProbe(host: ToolHost): Probe<TraceInput, ToolRaw> {
|
|
296
|
+
return defineProbe<TraceInput, ToolRaw>({
|
|
297
|
+
id: "path.traceroute",
|
|
298
|
+
version: "1",
|
|
299
|
+
summary: "The hops of the path to one IP address, from the tool traceroute or tracert.",
|
|
300
|
+
layer: "path",
|
|
301
|
+
mode: "active",
|
|
302
|
+
kinds: [PATH_TRACE],
|
|
303
|
+
tool: "traceroute",
|
|
304
|
+
peers: (input): readonly Target[] => [target(checked(input).address)],
|
|
305
|
+
available: (context) => tracerouteAvailability(host, context),
|
|
306
|
+
async run(input, context) {
|
|
307
|
+
const command = tracerouteCommand(host.platform, input);
|
|
308
|
+
if (command === undefined) throw new Error(`netdiag has no command of traceroute for ${host.platform}.`);
|
|
309
|
+
return runTool(host, command, context);
|
|
310
|
+
},
|
|
311
|
+
parse: (raw, input): readonly Observation[] => [observe(PATH_TRACE, target(input.address), readTraceroute(raw, input), { start: raw.start, end: raw.end })],
|
|
312
|
+
});
|
|
313
|
+
}
|