@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/src/tools/mtr.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { observe, type Observation } from "../fact.ts";
|
|
2
|
+
import { PATH_REPORT, type PathReport, type ReportHop } from "../kinds.ts";
|
|
3
|
+
import { defineProbe, type Availability, type Probe, type ProbeContext } from "../probe.ts";
|
|
4
|
+
import { target, type Target } from "../target.ts";
|
|
5
|
+
import { ENGLISH, firstLine, integerIn, sameAddress, toolAddress, type ToolAddress } from "./common.ts";
|
|
6
|
+
import { AVAILABILITY_TIMEOUT_MS, availabilityOf, runTool, TOOL_MARGIN_MS, type ToolCommand, type ToolHost, type ToolRaw } from "./host.ts";
|
|
7
|
+
import { DEFAULT_TRACE_HOPS, MAX_TRACE_HOPS } from "./traceroute.ts";
|
|
8
|
+
|
|
9
|
+
// The adapter of the tool mtr. It reads the JSON report of `mtr --json` (mtr 0.92 or later) on Linux and macOS. mtr
|
|
10
|
+
// sends one cycle of probes each second. A user without privileges cannot set a shorter interval.
|
|
11
|
+
|
|
12
|
+
/** The input of the mtr probe: many cycles of probes on the path to one IP address. */
|
|
13
|
+
export interface MtrInput {
|
|
14
|
+
readonly address: string;
|
|
15
|
+
/** The number of cycles. The default is `DEFAULT_MTR_CYCLES`. */
|
|
16
|
+
readonly cycles?: number | undefined;
|
|
17
|
+
/** The largest TTL. The default is `DEFAULT_TRACE_HOPS`. */
|
|
18
|
+
readonly maxHops?: number | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The default number of cycles. It is `LOSS_MIN_SENT` of the readers, so a report gives the feature `path.loss`. */
|
|
22
|
+
export const DEFAULT_MTR_CYCLES = 20;
|
|
23
|
+
|
|
24
|
+
/** The largest number of cycles of one input. */
|
|
25
|
+
export const MAX_MTR_CYCLES = 100;
|
|
26
|
+
|
|
27
|
+
/** The time that mtr waits for the last answers after the last cycle, in milliseconds (its option `--gracetime`). */
|
|
28
|
+
export const MTR_GRACE_MS = 5000;
|
|
29
|
+
|
|
30
|
+
interface CheckedInput extends ToolAddress {
|
|
31
|
+
readonly cycles: number;
|
|
32
|
+
readonly maxHops: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function checked(input: MtrInput): CheckedInput {
|
|
36
|
+
return {
|
|
37
|
+
...toolAddress(input.address),
|
|
38
|
+
cycles: integerIn(input.cycles ?? DEFAULT_MTR_CYCLES, 1, MAX_MTR_CYCLES, "a number of cycles"),
|
|
39
|
+
maxHops: integerIn(input.maxHops ?? DEFAULT_TRACE_HOPS, 1, MAX_TRACE_HOPS, "a number of hops"),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* This function gives the command of mtr for an operating system and an input. On Windows and on an operating system
|
|
45
|
+
* that the adapter does not know, it gives `undefined`. It throws for an input that is not valid.
|
|
46
|
+
*/
|
|
47
|
+
export function mtrCommand(platform: string, input: MtrInput): ToolCommand | undefined {
|
|
48
|
+
const { address, family, cycles, maxHops } = checked(input);
|
|
49
|
+
if (platform !== "linux" && platform !== "darwin") return undefined;
|
|
50
|
+
return {
|
|
51
|
+
command: "mtr",
|
|
52
|
+
args: ["--json", "--no-dns", family === "ipv6" ? "-6" : "-4", "-c", String(cycles), "-m", String(maxHops), address],
|
|
53
|
+
timeoutMs: cycles * 1000 + MTR_GRACE_MS + TOOL_MARGIN_MS,
|
|
54
|
+
env: ENGLISH,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** This function tells if mtr is available on the host. The adapter knows Linux and macOS. */
|
|
59
|
+
export async function mtrAvailability(host: ToolHost, context: ProbeContext): Promise<Availability> {
|
|
60
|
+
if (host.platform !== "linux" && host.platform !== "darwin") return { available: false, reason: `netdiag starts mtr only on Linux and macOS, not on ${host.platform}.` };
|
|
61
|
+
const output = await host.run("mtr", ["--version"], { timeoutMs: AVAILABILITY_TIMEOUT_MS, signal: context.signal, env: ENGLISH });
|
|
62
|
+
return availabilityOf("mtr", output, /mtr (\d[\d.]*)/);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function finite(value: unknown): number | undefined {
|
|
66
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** This function reads one hub of the JSON report, or gives `undefined` when netdiag cannot read it. */
|
|
70
|
+
function readHub(hub: unknown): ReportHop | undefined {
|
|
71
|
+
if (typeof hub !== "object" || hub === null) return undefined;
|
|
72
|
+
const record = hub as Readonly<Record<string, unknown>>;
|
|
73
|
+
// mtr 0.92 writes the count as a text, and mtr 0.93 and later as a number.
|
|
74
|
+
const ttl = Number(record["count"]);
|
|
75
|
+
const sent = record["Snt"];
|
|
76
|
+
const loss = finite(record["Loss%"]);
|
|
77
|
+
const host = record["host"];
|
|
78
|
+
if (!Number.isInteger(ttl) || ttl < 1 || typeof sent !== "number" || !Number.isInteger(sent) || sent < 0 || loss === undefined || loss > 100) return undefined;
|
|
79
|
+
if (typeof host !== "string") return undefined;
|
|
80
|
+
const received = sent - Math.round((sent * loss) / 100);
|
|
81
|
+
// mtr writes "???" for a hop with no answer.
|
|
82
|
+
if (host === "???") return received === 0 ? { ttl, sent, received } : undefined;
|
|
83
|
+
if (received === 0) return { ttl, address: host, sent, received };
|
|
84
|
+
const times = { bestMs: finite(record["Best"]), meanMs: finite(record["Avg"]), worstMs: finite(record["Wrst"]) };
|
|
85
|
+
return {
|
|
86
|
+
ttl,
|
|
87
|
+
address: host,
|
|
88
|
+
sent,
|
|
89
|
+
received,
|
|
90
|
+
...(times.bestMs === undefined ? {} : { bestMs: times.bestMs }),
|
|
91
|
+
...(times.meanMs === undefined ? {} : { meanMs: times.meanMs }),
|
|
92
|
+
...(times.worstMs === undefined ? {} : { worstMs: times.worstMs }),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** This function gives the hubs of the JSON report of mtr, or `undefined` when the text is not such a report. */
|
|
97
|
+
function hubsOf(text: string): readonly unknown[] | undefined {
|
|
98
|
+
try {
|
|
99
|
+
const parsed: unknown = JSON.parse(text);
|
|
100
|
+
if (typeof parsed !== "object" || parsed === null) return undefined;
|
|
101
|
+
const report = (parsed as { readonly report?: unknown }).report;
|
|
102
|
+
if (typeof report !== "object" || report === null) return undefined;
|
|
103
|
+
const hubs = (report as { readonly hubs?: unknown }).hubs;
|
|
104
|
+
return Array.isArray(hubs) ? hubs : undefined;
|
|
105
|
+
} catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* This function reads the raw output of the mtr probe into the value of a fact `path.report`.
|
|
112
|
+
*
|
|
113
|
+
* - The number of answers of a hop comes from its loss and its number of probes.
|
|
114
|
+
* - Output that netdiag cannot read gives the outcome `error`.
|
|
115
|
+
*/
|
|
116
|
+
export function readMtr(raw: ToolRaw, input: { readonly address: string; readonly cycles?: number | undefined }): PathReport {
|
|
117
|
+
const { address } = input;
|
|
118
|
+
const cycles = input.cycles ?? DEFAULT_MTR_CYCLES;
|
|
119
|
+
const error = (detail: string): PathReport => ({ address, outcome: "error", cycles, hops: [], detail });
|
|
120
|
+
if (raw.error !== undefined) return error(raw.error);
|
|
121
|
+
if (raw.timedOut === true) return error("The time limit stopped mtr before its report.");
|
|
122
|
+
const hubs = hubsOf(raw.stdout);
|
|
123
|
+
if (hubs === undefined) return error(firstLine(raw.stderr) ?? "netdiag cannot read the JSON report of mtr.");
|
|
124
|
+
const hops: ReportHop[] = [];
|
|
125
|
+
for (const hub of hubs) {
|
|
126
|
+
const hop = readHub(hub);
|
|
127
|
+
if (hop === undefined) return error(`netdiag cannot read the hub ${hops.length + 1} of the report of mtr.`);
|
|
128
|
+
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.`);
|
|
129
|
+
hops.push(hop);
|
|
130
|
+
}
|
|
131
|
+
const last = hops.at(-1);
|
|
132
|
+
if (last === undefined) return error(firstLine(raw.stderr) ?? "The report of mtr has no hub.");
|
|
133
|
+
const reached = last.address !== undefined && last.received > 0 && sameAddress(last.address, address);
|
|
134
|
+
return { address, outcome: reached ? "reached" : "stopped", cycles, hops };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** This function makes the probe `path.mtr`: many cycles of probes on the path to one IP address with the tool mtr. */
|
|
138
|
+
export function mtrProbe(host: ToolHost): Probe<MtrInput, ToolRaw> {
|
|
139
|
+
return defineProbe<MtrInput, ToolRaw>({
|
|
140
|
+
id: "path.mtr",
|
|
141
|
+
version: "1",
|
|
142
|
+
summary: "The loss and the round-trip times of each hop of the path to one IP address, from the tool mtr.",
|
|
143
|
+
layer: "path",
|
|
144
|
+
mode: "active",
|
|
145
|
+
kinds: [PATH_REPORT],
|
|
146
|
+
tool: "mtr",
|
|
147
|
+
peers: (input): readonly Target[] => [target(checked(input).address)],
|
|
148
|
+
available: (context) => mtrAvailability(host, context),
|
|
149
|
+
async run(input, context) {
|
|
150
|
+
const command = mtrCommand(host.platform, input);
|
|
151
|
+
if (command === undefined) throw new Error(`netdiag has no command of mtr for ${host.platform}.`);
|
|
152
|
+
return runTool(host, command, context);
|
|
153
|
+
},
|
|
154
|
+
parse: (raw, input): readonly Observation[] => [observe(PATH_REPORT, target(input.address), readMtr(raw, input), { start: raw.start, end: raw.end })],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
@@ -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
|
+
}
|