@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.
Files changed (45) hide show
  1. package/README.md +153 -1
  2. package/build/address.d.ts +56 -0
  3. package/build/address.d.ts.map +1 -0
  4. package/build/address.js +172 -0
  5. package/build/address.js.map +1 -0
  6. package/build/{diagnosis-BTa-Dfwl.js → diagnosis-a4hRzFxp.js} +113 -11
  7. package/build/diagnosis-a4hRzFxp.js.map +1 -0
  8. package/build/diagnosis.d.ts +3 -2
  9. package/build/diagnosis.js +1 -1
  10. package/build/{index-CP1hzELI.d.ts → index-K2dGiYxW.d.ts} +83 -22
  11. package/build/index-K2dGiYxW.d.ts.map +1 -0
  12. package/build/index.d.ts +253 -148
  13. package/build/index.d.ts.map +1 -1
  14. package/build/index.js +1084 -161
  15. package/build/index.js.map +1 -1
  16. package/build/kinds-CQv769lK.js +341 -0
  17. package/build/kinds-CQv769lK.js.map +1 -0
  18. package/build/kinds-_NShfZpU.d.ts +377 -0
  19. package/build/kinds-_NShfZpU.d.ts.map +1 -0
  20. package/build/layer-BNg0VYuK.d.ts +22 -0
  21. package/build/layer-BNg0VYuK.d.ts.map +1 -0
  22. package/build/tools.d.ts +206 -0
  23. package/build/tools.d.ts.map +1 -0
  24. package/build/tools.js +896 -0
  25. package/build/tools.js.map +1 -0
  26. package/package.json +17 -5
  27. package/src/address/index.ts +155 -0
  28. package/src/diagnosis/catalog.ts +8 -8
  29. package/src/diagnosis/index.ts +13 -0
  30. package/src/diagnosis/score.ts +160 -0
  31. package/src/index.ts +35 -0
  32. package/src/kinds.ts +460 -0
  33. package/src/probes/native.ts +354 -0
  34. package/src/probes/socket.ts +520 -0
  35. package/src/readers.ts +367 -0
  36. package/src/scope.ts +5 -24
  37. package/src/tools/common.ts +47 -0
  38. package/src/tools/host.ts +91 -0
  39. package/src/tools/index.ts +38 -0
  40. package/src/tools/licenses.ts +37 -0
  41. package/src/tools/mtr.ts +156 -0
  42. package/src/tools/ping.ts +231 -0
  43. package/src/tools/traceroute.ts +313 -0
  44. package/build/diagnosis-BTa-Dfwl.js.map +0 -1
  45. package/build/index-CP1hzELI.d.ts.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","names":["checked","AVAILABILITY","SEND_FAILURE","readUnix","readWindows","checked"],"sources":["../src/tools/host.ts","../src/tools/licenses.ts","../src/tools/common.ts","../src/tools/ping.ts","../src/tools/traceroute.ts","../src/tools/mtr.ts"],"sourcesContent":["import type { Availability, ProbeContext } from \"../probe.ts\";\n\n/**\n * The output of one command of an outside tool.\n *\n * The function `runCommand` of `@procedures/shell` gives this shape, so it is a host with no change (refer to the README).\n */\nexport interface ToolOutput {\n /** The exit code of the program. */\n readonly exitCode: number;\n readonly stdout: string;\n readonly stderr: string;\n /** This field is true when the time limit stopped the program. */\n readonly timedOut?: boolean | undefined;\n /** The error that kept the program from its start, for example `spawn ping ENOENT`. */\n readonly error?: string | undefined;\n}\n\n/** The options of one command. */\nexport interface ToolRunOptions {\n /** The time limit of the command, in milliseconds. After the limit, the host stops the program. */\n readonly timeoutMs: number;\n readonly signal: AbortSignal;\n /** The environment variables to add to the environment of the program, for example `LC_ALL=C` for English output. */\n readonly env?: Readonly<Record<string, string>> | undefined;\n}\n\n/**\n * The host of the outside tools: the operating system, and the function that starts a program.\n *\n * - `platform` has the values of `process.platform`, for example `linux`, `darwin` or `win32`.\n * - `run` starts a program with a list of arguments and no shell. It does not throw. A failure is in the output.\n */\nexport interface ToolHost {\n readonly platform: string;\n run(command: string, args: readonly string[], options: ToolRunOptions): Promise<ToolOutput>;\n}\n\n/** The raw output of a probe of an outside tool: the command, its output, and the times on the clock of the runner. */\nexport interface ToolRaw extends ToolOutput {\n readonly platform: string;\n readonly command: string;\n readonly args: readonly string[];\n readonly start: number;\n readonly end: number;\n}\n\n/** The time limit of the command that examines if a tool is available, in milliseconds. */\nexport const AVAILABILITY_TIMEOUT_MS = 5000;\n\n/** The time that an adapter adds to the time limit of a command, in milliseconds. In this time, a tool writes its last lines. */\nexport const TOOL_MARGIN_MS = 3000;\n\n/**\n * This function gives the availability of a tool from the output of a harmless command. A tool is available when its\n * program starts. The first group of `version` gives the version of the tool.\n */\nexport function availabilityOf(tool: string, output: ToolOutput, version?: RegExp): Availability {\n if (output.error !== undefined) return { available: false, reason: `The tool ${tool} does not start: ${output.error}` };\n const found = version?.exec(`${output.stdout}\\n${output.stderr}`)?.[1];\n return found === undefined ? { available: true } : { available: true, version: found };\n}\n\n/** A command of an outside tool: the program, its arguments, its time limit and the environment variables to add. */\nexport interface ToolCommand {\n readonly command: string;\n readonly args: readonly string[];\n readonly timeoutMs: number;\n readonly env?: Readonly<Record<string, string>> | undefined;\n}\n\n/** This function starts a command on the host, and gives its raw output with the times of the clock of the runner. */\nexport async function runTool(host: ToolHost, tool: ToolCommand, context: ProbeContext): Promise<ToolRaw> {\n const { command, args, timeoutMs, env } = tool;\n const start = context.clock.now();\n const output = await host.run(command, args, env === undefined ? { timeoutMs, signal: context.signal } : { timeoutMs, signal: context.signal, env });\n const end = context.clock.now();\n context.signal.throwIfAborted();\n return {\n platform: host.platform,\n command,\n args: [...args],\n start,\n end,\n exitCode: output.exitCode,\n stdout: output.stdout,\n stderr: output.stderr,\n ...(output.timedOut === true ? { timedOut: true } : {}),\n ...(output.error === undefined ? {} : { error: output.error }),\n };\n}\n","/**\n * The use of an outside tool in netdiag.\n *\n * - `adapter`: netdiag has an optional adapter. The user installs the tool, and netdiag starts it when it is available.\n * - `not-bundled`: netdiag does not start the tool, because its license is restrictive.\n */\nexport type ToolUse = \"adapter\" | \"not-bundled\";\n\n/** A row of the table of the licenses of the outside tools. */\nexport interface ToolLicense {\n /** The name of the program, for example `ping`. */\n readonly tool: string;\n readonly license: string;\n readonly use: ToolUse;\n /** The id of the probe of the adapter. A planned adapter has no probe yet. */\n readonly probe?: string | undefined;\n}\n\n/**\n * The licenses of the outside tools, as data. netdiag bundles no tool. A test makes sure that the probe of each adapter\n * has a row with the use `adapter`.\n */\nexport const TOOL_LICENSES: readonly ToolLicense[] = [\n { tool: \"ping\", license: \"The license of the operating system\", use: \"adapter\", probe: \"icmp.ping\" },\n { tool: \"traceroute\", license: \"GPL on Linux\", use: \"adapter\", probe: \"path.traceroute\" },\n { tool: \"mtr\", license: \"GPL\", use: \"adapter\", probe: \"path.mtr\" },\n { tool: \"tshark\", license: \"GPL\", use: \"adapter\" },\n { tool: \"iperf3\", license: \"BSD\", use: \"adapter\" },\n { tool: \"nmap\", license: \"NPSL\", use: \"not-bundled\" },\n { tool: \"masscan\", license: \"AGPL\", use: \"not-bundled\" },\n { tool: \"speedtest\", license: \"Ookla EULA\", use: \"not-bundled\" },\n];\n\n/** This function gives the row of a tool, or `undefined` for a tool that is not in the table. */\nexport function toolLicense(tool: string): ToolLicense | undefined {\n return TOOL_LICENSES.find((row) => row.tool === tool);\n}\n","import { addressFamily, canonicalAddress, type AddressFamily } from \"../address/index.ts\";\n\n// The parts that the adapters share. The entry `@mark1russell7/netdiag/tools` does not export this module.\n\n/** The environment that makes the output of a tool English on Linux and macOS. */\nexport const ENGLISH: Readonly<Record<string, string>> = { LC_ALL: \"C\" };\n\n/** The end of a line of output, on each operating system. */\nexport const LINES: RegExp = /\\r?\\n/;\n\n/** The messages of a local send that tell that the address is unreachable, on Linux and macOS. */\nexport const LOCAL_UNREACHABLE: RegExp = /\\b(Network is unreachable|No route to host|Host is down|Network is down)\\b/i;\n\n/** The IP address of a probe of an outside tool: its canonical form and its family. */\nexport interface ToolAddress {\n readonly address: string;\n readonly family: AddressFamily;\n}\n\n/**\n * This function gives the canonical form and the family of an IP address. It throws a `TypeError` for a name, because\n * an adapter sends probes only to an address.\n */\nexport function toolAddress(address: string): ToolAddress {\n const family = addressFamily(address);\n if (family === undefined) throw new TypeError(`\"${address}\" is not an IP address. Get the addresses of a name with the DNS probe first.`);\n return { address: canonicalAddress(address), family };\n}\n\n/** This function throws a `RangeError` when a value is not an integer from `min` to `max`. It gives the value. */\nexport function integerIn(value: number, min: number, max: number, what: string): number {\n if (!Number.isInteger(value) || value < min || value > max) throw new RangeError(`${value} is not ${what} from ${min} to ${max}.`);\n return value;\n}\n\n/** This function tells if two IP addresses are the same address. */\nexport function sameAddress(a: string, b: string): boolean {\n return canonicalAddress(a) === canonicalAddress(b);\n}\n\n/** This function gives the first line of a text that is not empty, with no spaces at its ends. */\nexport function firstLine(text: string): string | undefined {\n return text\n .split(LINES)\n .find((line) => line.trim().length > 0)\n ?.trim();\n}\n","import { isIP } from \"node:net\";\nimport { observe, type Observation } from \"../fact.ts\";\nimport { ICMP_ECHO, type IcmpEcho } from \"../kinds.ts\";\nimport { defineProbe, type Availability, type Probe, type ProbeContext } from \"../probe.ts\";\nimport { target, type Target } from \"../target.ts\";\nimport { ENGLISH, firstLine, integerIn, LINES, LOCAL_UNREACHABLE, toolAddress, type ToolAddress } from \"./common.ts\";\nimport { AVAILABILITY_TIMEOUT_MS, availabilityOf, runTool, TOOL_MARGIN_MS, type ToolCommand, type ToolHost, type ToolRaw } from \"./host.ts\";\n\n// The adapter of the tool `ping`. It reads the English output of iputils on Linux, of the BSD ping on macOS and of\n// ping.exe on Windows. On Linux and macOS, `LC_ALL=C` makes the output English. On Windows, the language of the\n// system sets the language of the output, so netdiag cannot read the output of a system in another language.\n\n/** The input of the ping probe: echo requests to one IP address. */\nexport interface PingInput {\n readonly address: string;\n /** The number of echo requests. The default is `DEFAULT_PING_COUNT`. */\n readonly count?: number | undefined;\n /** The time to wait for each reply, in milliseconds. The default is `DEFAULT_PING_WAIT_MS`. */\n readonly waitMs?: number | undefined;\n}\n\n/** The default number of echo requests. The feature `path.loss` needs `LOSS_MIN_SENT` requests or more. */\nexport const DEFAULT_PING_COUNT = 4;\n\n/** The default time to wait for each reply, in milliseconds. */\nexport const DEFAULT_PING_WAIT_MS = 1000;\n\n/** The largest number of echo requests in one input. */\nexport const MAX_PING_COUNT = 100;\n\n/** The time between two echo requests on Linux and macOS, in milliseconds. A user without privileges can set it. */\nexport const PING_INTERVAL_MS = 200;\n\n/**\n * The time that the `ping6` of macOS waits after its last request when a reply is missing, in milliseconds. The tool\n * has no option for it.\n */\nexport const PING6_LAST_WAIT_MS = 10_000;\n\ninterface CheckedInput extends ToolAddress {\n readonly count: number;\n readonly waitMs: number;\n}\n\nfunction checked(input: PingInput): CheckedInput {\n return {\n ...toolAddress(input.address),\n count: integerIn(input.count ?? DEFAULT_PING_COUNT, 1, MAX_PING_COUNT, \"a number of requests\"),\n waitMs: integerIn(input.waitMs ?? DEFAULT_PING_WAIT_MS, 1, 60_000, \"a time in milliseconds\"),\n };\n}\n\n/**\n * This function gives the command of ping for an operating system and an input. For an operating system that the adapter\n * does not know, it gives `undefined`. It throws for an input that is not valid.\n *\n * - Linux: iputils `ping`, with a deadline (`-w`) in seconds, so that ping writes its statistics.\n * - macOS: `ping` with a deadline (`-t`), and `ping6` for an IPv6 address.\n * - Windows: `ping.exe`. It sends one request each second.\n */\nexport function pingCommand(platform: string, input: PingInput): ToolCommand | undefined {\n const { address, family, count, waitMs } = checked(input);\n const interval = (count - 1) * PING_INTERVAL_MS;\n const deadlineS = Math.ceil((interval + waitMs) / 1000) + 1;\n const seconds = (ms: number): string => String(ms / 1000);\n switch (platform) {\n case \"linux\":\n return {\n command: \"ping\",\n args: [\"-n\", \"-c\", String(count), \"-i\", seconds(PING_INTERVAL_MS), \"-W\", String(Math.ceil(waitMs / 1000)), \"-w\", String(deadlineS), address],\n timeoutMs: deadlineS * 1000 + TOOL_MARGIN_MS,\n env: ENGLISH,\n };\n case \"darwin\":\n return family === \"ipv4\"\n ? {\n command: \"ping\",\n args: [\"-n\", \"-c\", String(count), \"-i\", seconds(PING_INTERVAL_MS), \"-W\", String(waitMs), \"-t\", String(deadlineS), address],\n timeoutMs: deadlineS * 1000 + TOOL_MARGIN_MS,\n env: ENGLISH,\n }\n : { command: \"ping6\", args: [\"-n\", \"-c\", String(count), address], timeoutMs: (count - 1) * 1000 + PING6_LAST_WAIT_MS + TOOL_MARGIN_MS, env: ENGLISH };\n case \"win32\":\n return { command: \"ping\", args: [\"-n\", String(count), \"-w\", String(waitMs), address], timeoutMs: count * Math.max(1000, waitMs) + waitMs + TOOL_MARGIN_MS };\n default:\n return undefined;\n }\n}\n\n/** The command that examines if ping is available on each operating system, and the pattern of its version. */\nconst AVAILABILITY: Readonly<Record<string, { readonly args: readonly string[]; readonly version?: RegExp }>> = {\n linux: { args: [\"-V\"], version: /iputils\\D*(\\d+)/i },\n darwin: { args: [] },\n win32: { args: [\"/?\"] },\n};\n\n/** This function tells if ping is available on the host. On Linux, netdiag reads only the ping of iputils. */\nexport async function pingAvailability(host: ToolHost, context: ProbeContext): Promise<Availability> {\n const check = AVAILABILITY[host.platform];\n if (check === undefined) return { available: false, reason: `netdiag reads the output of ping only on Linux, macOS and Windows, not on ${host.platform}.` };\n const output = await host.run(\"ping\", check.args, { timeoutMs: AVAILABILITY_TIMEOUT_MS, signal: context.signal, env: ENGLISH });\n if (host.platform === \"linux\" && output.error === undefined && !/iputils/i.test(`${output.stdout}\\n${output.stderr}`)) {\n return { available: false, reason: \"netdiag reads only the ping of iputils on Linux. This ping does not tell an iputils version.\" };\n }\n return availabilityOf(\"ping\", output, check.version);\n}\n\n/** The replies and the messages that netdiag reads in the output of ping. */\ninterface PingLines {\n /** The number of requests from the statistics. Without statistics, netdiag cannot read the output. */\n readonly sent?: number | undefined;\n readonly replies: readonly { readonly rttMs: number; readonly ttl?: number | undefined }[];\n readonly unreachable?: { readonly from?: string | undefined; readonly detail: string } | undefined;\n /** A failure of this computer to send a request, for example a firewall rule. */\n readonly failure?: string | undefined;\n}\n\n/** The messages of a failed send of ping on Linux and macOS, for example `socket: Operation not permitted`. */\nconst SEND_FAILURE = /^(?:socket|bind|connect|sendmsg|sendto|send): \\S/;\n\nfunction readUnix(stdout: string, stderr: string): PingLines {\n const replies: { rttMs: number; ttl?: number }[] = [];\n let unreachable: PingLines[\"unreachable\"];\n let failure: string | undefined;\n for (const line of stdout.split(LINES)) {\n // Each pattern has one way to match, so a long line cannot make it slow. A second test examines the rest of the line.\n // iputils: \"From 192.0.2.5 icmp_seq=1 Destination Host Unreachable\". macOS: \"92 bytes from 192.0.2.5: Destination Host Unreachable\".\n const head = /^\\d+ bytes from (\\S+) (.*)$/.exec(line) ?? /^From (\\S+) icmp_seq=\\d+ (.*)$/.exec(line);\n if (head === null) continue;\n const from = withoutSeparator(head[1] ?? \"\");\n const rest = head[2] ?? \"\";\n const time = line.startsWith(\"From \") ? null : /\\btime[=<]([\\d.]+) ?ms/.exec(rest);\n if (time !== null) {\n if (rest.includes(\"DUP!\")) continue;\n const ttl = /\\b(?:ttl|hlim)=(\\d+)/.exec(rest)?.[1];\n replies.push(ttl === undefined ? { rttMs: Number(time[1]) } : { rttMs: Number(time[1]), ttl: Number(ttl) });\n } else if (/unreachable/i.test(rest) && unreachable === undefined) {\n unreachable = { from, detail: rest.trim() };\n }\n }\n // A warning on stderr is not a failure. Only a failed send is a failure.\n for (const line of stderr.split(LINES)) {\n const text = line.replace(/^ping6?:\\s*/, \"\").trim();\n const local = LOCAL_UNREACHABLE.exec(text);\n if (local !== null) unreachable ??= { detail: local[1] ?? text };\n else if (SEND_FAILURE.test(text)) failure ??= text;\n }\n const sent = /^(\\d+) packets transmitted, \\d+ (?:packets )?received/m.exec(stdout)?.[1];\n return { sent: sent === undefined ? undefined : Number(sent), replies, unreachable, failure };\n}\n\n/** This function removes the `:` or `,` after an address in a line of ping. An IPv6 address keeps its own last `:`. */\nfunction withoutSeparator(text: string): string {\n return isIP(text) === 0 ? text.replace(/[:,]$/, \"\") : text;\n}\n\nfunction readWindows(stdout: string): PingLines {\n const replies: { rttMs: number; ttl?: number }[] = [];\n let unreachable: PingLines[\"unreachable\"];\n let failure: string | undefined;\n for (const line of stdout.split(LINES)) {\n // \"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.\n const reply = /^Reply from (\\S+?): (?:bytes=\\d+ )?time([=<])(\\d+)ms(?: TTL=(\\d+))?/.exec(line);\n if (reply !== null) {\n const ttl = reply[4];\n replies.push(ttl === undefined ? { rttMs: Number(reply[3]) } : { rttMs: Number(reply[3]), ttl: Number(ttl) });\n continue;\n }\n // \"Reply from 192.168.1.191: Destination host unreachable.\" or \"Destination net unreachable.\"\n const text = line.trim();\n const message = /^(?:Reply from (\\S+): )?(Destination .*)$/i.exec(text);\n if (message !== null && unreachable === undefined && /unreachable\\.?$/i.test(message[2] ?? \"\")) {\n const detail = (message[2] ?? \"\").replace(/\\.$/, \"\");\n unreachable = message[1] === undefined ? { detail } : { from: message[1], detail };\n }\n if (text.startsWith(\"PING: transmit failed. \")) failure ??= text.slice(\"PING: \".length);\n }\n const sent = /Packets: Sent = (\\d+), Received = \\d+/.exec(stdout)?.[1];\n return { sent: sent === undefined ? undefined : Number(sent), replies, unreachable, failure };\n}\n\n/**\n * This function reads the raw output of the ping probe into the value of a fact `icmp.echo`.\n *\n * - The replies are the lines of echo replies. A message of an unreachable address and a duplicate are not replies.\n * - Output with no statistics gives the outcome `error`, also when it has replies. The time limit stopped such a ping.\n */\nexport function readPing(raw: ToolRaw, address: string): IcmpEcho {\n const error = (detail: string): IcmpEcho => ({ address, outcome: \"error\", sent: 0, received: 0, rttMs: [], detail });\n if (raw.error !== undefined) return error(raw.error);\n const lines = raw.platform === \"win32\" ? readWindows(raw.stdout) : readUnix(raw.stdout, raw.stderr);\n const { sent, replies, unreachable, failure } = lines;\n const from = unreachable?.from === undefined ? {} : { from: unreachable.from };\n if (sent === undefined) {\n if (unreachable !== undefined && raw.timedOut !== true) return { address, outcome: \"unreachable\", sent: 0, received: 0, rttMs: [], ...from, detail: unreachable.detail };\n if (raw.timedOut === true) return error(\"The time limit stopped ping before its statistics.\");\n return error(failure ?? firstLine(raw.stderr) ?? \"netdiag cannot read the output of ping. netdiag reads only English output.\");\n }\n if (replies.length > sent) return error(`The output of ping has ${replies.length} replies to ${sent} requests.`);\n const first = replies[0];\n if (first !== undefined) {\n return { address, outcome: \"reply\", sent, received: replies.length, rttMs: replies.map((r) => r.rttMs), ...(first.ttl === undefined ? {} : { ttl: first.ttl }) };\n }\n if (unreachable !== undefined) return { address, outcome: \"unreachable\", sent, received: 0, rttMs: [], ...from, detail: unreachable.detail };\n if (failure !== undefined) return error(failure);\n return { address, outcome: \"none\", sent, received: 0, rttMs: [] };\n}\n\n/**\n * This function makes the probe `icmp.ping`: the echo requests of the tool ping to one IP address. The host starts\n * the tool. Give `runCommand` of `@procedures/shell`, or a fake host in a test.\n */\nexport function pingProbe(host: ToolHost): Probe<PingInput, ToolRaw> {\n return defineProbe<PingInput, ToolRaw>({\n id: \"icmp.ping\",\n version: \"1\",\n summary: \"The ICMP echo requests of the tool ping to one IP address.\",\n layer: \"ip\",\n mode: \"active\",\n kinds: [ICMP_ECHO],\n tool: \"ping\",\n peers: (input): readonly Target[] => [target(checked(input).address)],\n available: (context) => pingAvailability(host, context),\n async run(input, context) {\n const command = pingCommand(host.platform, input);\n if (command === undefined) throw new Error(`netdiag has no command of ping for ${host.platform}.`);\n return runTool(host, command, context);\n },\n parse: (raw, input): readonly Observation[] => [observe(ICMP_ECHO, target(input.address), readPing(raw, input.address), { start: raw.start, end: raw.end })],\n });\n}\n","import { isIP } from \"node:net\";\nimport { observe, type Observation } from \"../fact.ts\";\nimport { PATH_TRACE, type PathTrace, type TraceHop } from \"../kinds.ts\";\nimport { defineProbe, type Availability, type Probe, type ProbeContext } from \"../probe.ts\";\nimport { target, type Target } from \"../target.ts\";\nimport { ENGLISH, firstLine, integerIn, LINES, LOCAL_UNREACHABLE, sameAddress, toolAddress, type ToolAddress } from \"./common.ts\";\nimport { AVAILABILITY_TIMEOUT_MS, availabilityOf, runTool, TOOL_MARGIN_MS, type ToolCommand, type ToolHost, type ToolRaw } from \"./host.ts\";\n\n// The adapter of the tool traceroute. It reads the English output of the Modern traceroute for Linux, of the BSD\n// traceroute and traceroute6 on macOS, and of tracert.exe on Windows. Each tool sends `TRACE_QUERIES` probes for each TTL.\n\n/** The input of the traceroute probe: the path to one IP address. */\nexport interface TraceInput {\n readonly address: string;\n /** The largest TTL. The default is `DEFAULT_TRACE_HOPS`. */\n readonly maxHops?: number | undefined;\n /** The time to wait for each answer, in milliseconds. On Linux and macOS, the tool rounds it up to full seconds. */\n readonly waitMs?: number | undefined;\n}\n\n/** The default largest TTL. */\nexport const DEFAULT_TRACE_HOPS = 30;\n\n/** The largest TTL of one input. */\nexport const MAX_TRACE_HOPS = 64;\n\n/** The default time to wait for each answer, in milliseconds. */\nexport const DEFAULT_TRACE_WAIT_MS = 1000;\n\n/** The number of probes for each TTL. `tracert` on Windows always sends 3 probes. */\nexport const TRACE_QUERIES = 3;\n\ninterface CheckedInput extends ToolAddress {\n readonly maxHops: number;\n readonly waitMs: number;\n}\n\nfunction checked(input: TraceInput): CheckedInput {\n return {\n ...toolAddress(input.address),\n maxHops: integerIn(input.maxHops ?? DEFAULT_TRACE_HOPS, 1, MAX_TRACE_HOPS, \"a number of hops\"),\n waitMs: integerIn(input.waitMs ?? DEFAULT_TRACE_WAIT_MS, 1, 10_000, \"a time in milliseconds\"),\n };\n}\n\n/**\n * This function gives the command of traceroute for an operating system and an input. For an operating system that the\n * adapter does not know, it gives `undefined`. It throws for an input that is not valid.\n *\n * - Linux: `traceroute`, with UDP probes. A user without privileges can send them.\n * - macOS: `traceroute`, and `traceroute6` for an IPv6 address.\n * - Windows: `tracert.exe`, with ICMP echo requests.\n *\n * The time limit is the time of a trace that waits for each probe of each hop.\n */\nexport function tracerouteCommand(platform: string, input: TraceInput): ToolCommand | undefined {\n const { address, family, maxHops, waitMs } = checked(input);\n const waitS = Math.ceil(waitMs / 1000);\n const unix = [\"-n\", \"-q\", String(TRACE_QUERIES), \"-w\", String(waitS), \"-m\", String(maxHops)];\n const unixTimeoutMs = maxHops * TRACE_QUERIES * waitS * 1000 + TOOL_MARGIN_MS;\n switch (platform) {\n case \"linux\":\n return { command: \"traceroute\", args: [family === \"ipv6\" ? \"-6\" : \"-4\", ...unix, address], timeoutMs: unixTimeoutMs, env: ENGLISH };\n case \"darwin\":\n return { command: family === \"ipv6\" ? \"traceroute6\" : \"traceroute\", args: [...unix, address], timeoutMs: unixTimeoutMs, env: ENGLISH };\n case \"win32\":\n return { command: \"tracert\", args: [\"-d\", \"-h\", String(maxHops), \"-w\", String(waitMs), address], timeoutMs: maxHops * TRACE_QUERIES * waitMs + TOOL_MARGIN_MS };\n default:\n return undefined;\n }\n}\n\n/** The command that examines if traceroute is available on each operating system, and the pattern of its version. */\nconst AVAILABILITY: Readonly<Record<string, { readonly command: string; readonly args: readonly string[]; readonly version?: RegExp }>> = {\n linux: { command: \"traceroute\", args: [\"--version\"], version: /version (\\d[\\d.]*)/ },\n darwin: { command: \"traceroute\", args: [] },\n win32: { command: \"tracert\", args: [\"/?\"] },\n};\n\n/** This function tells if traceroute is available on the host. On Linux, netdiag reads only the Modern traceroute for Linux. */\nexport async function tracerouteAvailability(host: ToolHost, context: ProbeContext): Promise<Availability> {\n const check = AVAILABILITY[host.platform];\n if (check === undefined) return { available: false, reason: `netdiag reads the output of traceroute only on Linux, macOS and Windows, not on ${host.platform}.` };\n const output = await host.run(check.command, check.args, { timeoutMs: AVAILABILITY_TIMEOUT_MS, signal: context.signal, env: ENGLISH });\n if (host.platform === \"linux\" && output.error === undefined && !/Modern traceroute/i.test(`${output.stdout}\\n${output.stderr}`)) {\n return { available: false, reason: \"netdiag reads only the Modern traceroute for Linux (the package traceroute). This traceroute does not tell that name.\" };\n }\n return availabilityOf(check.command, output, check.version);\n}\n\n/** The flags of the Linux and BSD traceroute after the time of an answer, and their meaning. */\nconst FLAGS: Readonly<Record<string, string>> = {\n \"!H\": \"host unreachable\",\n \"!N\": \"network unreachable\",\n \"!P\": \"protocol unreachable\",\n \"!S\": \"source route failed\",\n \"!F\": \"fragmentation needed\",\n \"!X\": \"communication administratively prohibited\",\n \"!A\": \"communication with the network administratively prohibited\",\n \"!Z\": \"communication with the host administratively prohibited\",\n \"!U\": \"destination network unknown\",\n \"!W\": \"destination host unknown\",\n \"!V\": \"host precedence violation\",\n \"!C\": \"precedence cutoff\",\n};\n\n/** 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. */\nfunction flagText(flag: string): string | undefined {\n if (flag === \"!\") return undefined;\n const code = /^!<?(\\d+)>?$/.exec(flag)?.[1];\n if (code !== undefined) return `unreachable, ICMP code ${code} (${flag})`;\n const known = FLAGS[flag.replace(/^(!F)-?\\d+$/, \"$1\")];\n return `${known ?? \"unreachable\"} (${flag})`;\n}\n\n/** The hops that netdiag reads in the output of a trace, and the facts about the output. */\ninterface TraceLines {\n readonly hops: readonly TraceHop[];\n /** This field is true when the output has the header of the tool. Output in another language has no such header. */\n readonly header: boolean;\n /** This field is true when the output has the last line of a trace (`tracert` only). */\n readonly complete: boolean;\n /** The first hop that netdiag cannot read. */\n readonly unread?: number | undefined;\n /** A failure of this computer to send a probe. */\n readonly failure?: string | undefined;\n /** A message of this computer that tells that the address is unreachable. */\n readonly local?: string | undefined;\n}\n\n/** One hop while the reader reads it. */\nclass HopBuilder {\n readonly addresses: string[] = [];\n readonly rttMs: number[] = [];\n sent = 0;\n received = 0;\n unreachable: string | undefined;\n /** This field is true when a time comes before an address. */\n unread = false;\n readonly ttl: number;\n private current: string | undefined;\n\n constructor(ttl: number) {\n this.ttl = ttl;\n }\n\n address(text: string): void {\n this.current = text;\n if (!this.addresses.some((known) => sameAddress(known, text))) this.addresses.push(text);\n }\n\n lost(): void {\n this.sent += 1;\n }\n\n answer(rttMs: number | undefined): void {\n this.sent += 1;\n if (this.current === undefined) {\n this.unread = true;\n return;\n }\n this.received += 1;\n if (rttMs !== undefined) this.rttMs.push(rttMs);\n }\n\n hop(): TraceHop {\n const hop = { ttl: this.ttl, addresses: [...this.addresses], sent: this.sent, received: this.received, rttMs: [...this.rttMs] };\n return this.unreachable === undefined ? hop : { ...hop, unreachable: this.unreachable };\n }\n}\n\n/** This function gives the address of a token, for example `192.0.2.1` or `(192.0.2.1)`, or `undefined`. */\nfunction addressOf(token: string): string | undefined {\n const text = token.replace(/^\\((.*)\\)$/, \"$1\");\n return isIP(text) === 0 ? undefined : text;\n}\n\n/** The messages of traceroute on Linux and macOS when a socket operation fails, for example `socket: Operation not permitted`. */\nconst SEND_FAILURE = /^(?:socket|bind|connect|sendmsg|sendto|send): \\S/;\n\nfunction readUnix(stdout: string, stderr: string): TraceLines {\n const builders: HopBuilder[] = [];\n for (const line of stdout.split(LINES)) {\n const numbered = /^\\s*(\\d+)\\s(.*)$/.exec(line);\n // macOS writes another address of the same TTL on a line with no number.\n const continued = numbered === null && /^\\s+\\S/.test(line) ? builders.at(-1) : undefined;\n if (numbered === null && continued === undefined) continue;\n const builder = continued ?? new HopBuilder(Number(numbered?.[1]));\n if (continued === undefined) builders.push(builder);\n const tokens = (numbered?.[2] ?? line).trim().split(/\\s+/);\n for (let i = 0; i < tokens.length; i += 1) {\n const token = tokens[i] ?? \"\";\n const address = addressOf(token);\n const time = /^(\\d+(?:\\.\\d+)?)(ms)?$/.exec(token);\n if (token === \"*\") builder.lost();\n else if (address !== undefined) builder.address(address);\n else if (time !== null && (time[2] === \"ms\" || tokens[i + 1] === \"ms\")) {\n builder.answer(Number(time[1]));\n if (time[2] === undefined) i += 1;\n } else if (token.startsWith(\"!\")) builder.unreachable ??= flagText(token);\n }\n }\n let failure: string | undefined;\n let local: string | undefined;\n for (const line of stderr.split(LINES)) {\n const text = line.replace(/^traceroute6?:\\s*/, \"\").trim();\n const unreachable = LOCAL_UNREACHABLE.exec(text);\n if (unreachable !== null) local ??= unreachable[1] ?? text;\n else if (SEND_FAILURE.test(text)) failure ??= text;\n }\n const header = /^traceroute6? to /m.test(`${stdout}\\n${stderr}`);\n return { hops: builders.map((b) => b.hop()), header, complete: true, unread: builders.find((b) => b.unread)?.ttl, failure, local };\n}\n\n/** The text between the address of a router and its message in the output of `tracert`. */\nconst REPORTS = \" reports: \";\n\nfunction readWindows(stdout: string): TraceLines {\n const builders: HopBuilder[] = [];\n let failure: string | undefined;\n for (const line of stdout.split(LINES)) {\n const numbered = /^\\s*(\\d+)\\s(.*)$/.exec(line);\n if (numbered === null) continue;\n const builder = new HopBuilder(Number(numbered[1]));\n builders.push(builder);\n // \" 2 * * 192.168.1.191 reports: Destination host unreachable.\"\n const rest = (numbered[2] ?? \"\").trim();\n const at = rest.indexOf(REPORTS);\n const tokens = (at < 0 ? rest : rest.slice(0, at)).split(/\\s+/).filter((token) => token.length > 0);\n // tracert writes the address after the times of the hop.\n const address = tokens.map(addressOf).find((found) => found !== undefined);\n if (address !== undefined) builder.address(address);\n for (let i = 0; i < tokens.length; i += 1) {\n const token = tokens[i] ?? \"\";\n // \"<1 ms\" counts as 1 ms, as in the output of ping.\n const time = /^<?(\\d+)$/.exec(token);\n if (token === \"*\") builder.lost();\n else if (time !== null && tokens[i + 1] === \"ms\") {\n builder.answer(Math.max(1, Number(time[1])));\n i += 1;\n } else if (/^(General failure|Transmit error)/i.test(tokens.slice(i).join(\" \"))) {\n failure ??= tokens.slice(i).join(\" \").replace(/\\.$/, \"\");\n break;\n }\n }\n if (at >= 0) {\n builder.answer(undefined);\n builder.unreachable = rest\n .slice(at + REPORTS.length)\n .trim()\n .replace(/\\.$/, \"\");\n }\n }\n return {\n hops: builders.map((b) => b.hop()),\n header: /^Tracing route to /m.test(stdout),\n complete: /^Trace complete\\./m.test(stdout),\n unread: builders.find((b) => b.unread)?.ttl,\n failure,\n };\n}\n\n/**\n * This function reads the raw output of the traceroute probe into the value of a fact `path.trace`.\n *\n * - The outcome is `reached` when the address answered at the last hop.\n * - Output that netdiag cannot read gives the outcome `error`. An `error` keeps the hops that netdiag read.\n */\nexport function readTraceroute(raw: ToolRaw, input: { readonly address: string; readonly maxHops?: number | undefined }): PathTrace {\n const { address } = input;\n const maxHops = input.maxHops ?? DEFAULT_TRACE_HOPS;\n const result = (outcome: PathTrace[\"outcome\"], hops: readonly TraceHop[], detail?: string): PathTrace =>\n detail === undefined ? { address, outcome, maxHops, hops } : { address, outcome, maxHops, hops, detail };\n if (raw.error !== undefined) return result(\"error\", [], raw.error);\n const lines = raw.platform === \"win32\" ? readWindows(raw.stdout) : readUnix(raw.stdout, raw.stderr);\n const { hops, header, complete, unread, failure, local } = lines;\n if (unread !== undefined) return result(\"error\", hops.filter((hop) => hop.ttl < unread), `netdiag cannot read the hop ${unread} of traceroute.`);\n // Output in another language has numbered lines too, but no English header.\n if (!header && local === undefined) {\n return result(\"error\", [], failure ?? firstLine(raw.stderr) ?? \"netdiag cannot read the output of traceroute. netdiag reads only English output.\");\n }\n if (raw.timedOut === true || !complete) return result(\"error\", hops, \"The time limit stopped traceroute before its end.\");\n const last = hops.at(-1);\n if (last?.addresses.some((known) => sameAddress(known, address)) === true) return result(\"reached\", hops);\n if (last?.unreachable !== undefined) return result(\"unreachable\", hops, last.unreachable);\n if (local !== undefined) return result(\"unreachable\", hops, local);\n if (failure !== undefined) return result(\"error\", hops, failure);\n return result(\"stopped\", hops);\n}\n\n/**\n * This function makes the probe `path.traceroute`: the trace of the path to one IP address with the tool traceroute,\n * or `tracert` on Windows. The host starts the tool.\n */\nexport function tracerouteProbe(host: ToolHost): Probe<TraceInput, ToolRaw> {\n return defineProbe<TraceInput, ToolRaw>({\n id: \"path.traceroute\",\n version: \"1\",\n summary: \"The hops of the path to one IP address, from the tool traceroute or tracert.\",\n layer: \"path\",\n mode: \"active\",\n kinds: [PATH_TRACE],\n tool: \"traceroute\",\n peers: (input): readonly Target[] => [target(checked(input).address)],\n available: (context) => tracerouteAvailability(host, context),\n async run(input, context) {\n const command = tracerouteCommand(host.platform, input);\n if (command === undefined) throw new Error(`netdiag has no command of traceroute for ${host.platform}.`);\n return runTool(host, command, context);\n },\n parse: (raw, input): readonly Observation[] => [observe(PATH_TRACE, target(input.address), readTraceroute(raw, input), { start: raw.start, end: raw.end })],\n });\n}\n","import { observe, type Observation } from \"../fact.ts\";\nimport { PATH_REPORT, type PathReport, type ReportHop } from \"../kinds.ts\";\nimport { defineProbe, type Availability, type Probe, type ProbeContext } from \"../probe.ts\";\nimport { target, type Target } from \"../target.ts\";\nimport { ENGLISH, firstLine, integerIn, sameAddress, toolAddress, type ToolAddress } from \"./common.ts\";\nimport { AVAILABILITY_TIMEOUT_MS, availabilityOf, runTool, TOOL_MARGIN_MS, type ToolCommand, type ToolHost, type ToolRaw } from \"./host.ts\";\nimport { DEFAULT_TRACE_HOPS, MAX_TRACE_HOPS } from \"./traceroute.ts\";\n\n// The adapter of the tool mtr. It reads the JSON report of `mtr --json` (mtr 0.92 or later) on Linux and macOS. mtr\n// sends one cycle of probes each second. A user without privileges cannot set a shorter interval.\n\n/** The input of the mtr probe: many cycles of probes on the path to one IP address. */\nexport interface MtrInput {\n readonly address: string;\n /** The number of cycles. The default is `DEFAULT_MTR_CYCLES`. */\n readonly cycles?: number | undefined;\n /** The largest TTL. The default is `DEFAULT_TRACE_HOPS`. */\n readonly maxHops?: number | undefined;\n}\n\n/** The default number of cycles. It is `LOSS_MIN_SENT` of the readers, so a report gives the feature `path.loss`. */\nexport const DEFAULT_MTR_CYCLES = 20;\n\n/** The largest number of cycles of one input. */\nexport const MAX_MTR_CYCLES = 100;\n\n/** The time that mtr waits for the last answers after the last cycle, in milliseconds (its option `--gracetime`). */\nexport const MTR_GRACE_MS = 5000;\n\ninterface CheckedInput extends ToolAddress {\n readonly cycles: number;\n readonly maxHops: number;\n}\n\nfunction checked(input: MtrInput): CheckedInput {\n return {\n ...toolAddress(input.address),\n cycles: integerIn(input.cycles ?? DEFAULT_MTR_CYCLES, 1, MAX_MTR_CYCLES, \"a number of cycles\"),\n maxHops: integerIn(input.maxHops ?? DEFAULT_TRACE_HOPS, 1, MAX_TRACE_HOPS, \"a number of hops\"),\n };\n}\n\n/**\n * This function gives the command of mtr for an operating system and an input. On Windows and on an operating system\n * that the adapter does not know, it gives `undefined`. It throws for an input that is not valid.\n */\nexport function mtrCommand(platform: string, input: MtrInput): ToolCommand | undefined {\n const { address, family, cycles, maxHops } = checked(input);\n if (platform !== \"linux\" && platform !== \"darwin\") return undefined;\n return {\n command: \"mtr\",\n args: [\"--json\", \"--no-dns\", family === \"ipv6\" ? \"-6\" : \"-4\", \"-c\", String(cycles), \"-m\", String(maxHops), address],\n timeoutMs: cycles * 1000 + MTR_GRACE_MS + TOOL_MARGIN_MS,\n env: ENGLISH,\n };\n}\n\n/** This function tells if mtr is available on the host. The adapter knows Linux and macOS. */\nexport async function mtrAvailability(host: ToolHost, context: ProbeContext): Promise<Availability> {\n if (host.platform !== \"linux\" && host.platform !== \"darwin\") return { available: false, reason: `netdiag starts mtr only on Linux and macOS, not on ${host.platform}.` };\n const output = await host.run(\"mtr\", [\"--version\"], { timeoutMs: AVAILABILITY_TIMEOUT_MS, signal: context.signal, env: ENGLISH });\n return availabilityOf(\"mtr\", output, /mtr (\\d[\\d.]*)/);\n}\n\nfunction finite(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : undefined;\n}\n\n/** This function reads one hub of the JSON report, or gives `undefined` when netdiag cannot read it. */\nfunction readHub(hub: unknown): ReportHop | undefined {\n if (typeof hub !== \"object\" || hub === null) return undefined;\n const record = hub as Readonly<Record<string, unknown>>;\n // mtr 0.92 writes the count as a text, and mtr 0.93 and later as a number.\n const ttl = Number(record[\"count\"]);\n const sent = record[\"Snt\"];\n const loss = finite(record[\"Loss%\"]);\n const host = record[\"host\"];\n if (!Number.isInteger(ttl) || ttl < 1 || typeof sent !== \"number\" || !Number.isInteger(sent) || sent < 0 || loss === undefined || loss > 100) return undefined;\n if (typeof host !== \"string\") return undefined;\n const received = sent - Math.round((sent * loss) / 100);\n // mtr writes \"???\" for a hop with no answer.\n if (host === \"???\") return received === 0 ? { ttl, sent, received } : undefined;\n if (received === 0) return { ttl, address: host, sent, received };\n const times = { bestMs: finite(record[\"Best\"]), meanMs: finite(record[\"Avg\"]), worstMs: finite(record[\"Wrst\"]) };\n return {\n ttl,\n address: host,\n sent,\n received,\n ...(times.bestMs === undefined ? {} : { bestMs: times.bestMs }),\n ...(times.meanMs === undefined ? {} : { meanMs: times.meanMs }),\n ...(times.worstMs === undefined ? {} : { worstMs: times.worstMs }),\n };\n}\n\n/** This function gives the hubs of the JSON report of mtr, or `undefined` when the text is not such a report. */\nfunction hubsOf(text: string): readonly unknown[] | undefined {\n try {\n const parsed: unknown = JSON.parse(text);\n if (typeof parsed !== \"object\" || parsed === null) return undefined;\n const report = (parsed as { readonly report?: unknown }).report;\n if (typeof report !== \"object\" || report === null) return undefined;\n const hubs = (report as { readonly hubs?: unknown }).hubs;\n return Array.isArray(hubs) ? hubs : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * This function reads the raw output of the mtr probe into the value of a fact `path.report`.\n *\n * - The number of answers of a hop comes from its loss and its number of probes.\n * - Output that netdiag cannot read gives the outcome `error`.\n */\nexport function readMtr(raw: ToolRaw, input: { readonly address: string; readonly cycles?: number | undefined }): PathReport {\n const { address } = input;\n const cycles = input.cycles ?? DEFAULT_MTR_CYCLES;\n const error = (detail: string): PathReport => ({ address, outcome: \"error\", cycles, hops: [], detail });\n if (raw.error !== undefined) return error(raw.error);\n if (raw.timedOut === true) return error(\"The time limit stopped mtr before its report.\");\n const hubs = hubsOf(raw.stdout);\n if (hubs === undefined) return error(firstLine(raw.stderr) ?? \"netdiag cannot read the JSON report of mtr.\");\n const hops: ReportHop[] = [];\n for (const hub of hubs) {\n const hop = readHub(hub);\n if (hop === undefined) return error(`netdiag cannot read the hub ${hops.length + 1} of the report of mtr.`);\n 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.`);\n hops.push(hop);\n }\n const last = hops.at(-1);\n if (last === undefined) return error(firstLine(raw.stderr) ?? \"The report of mtr has no hub.\");\n const reached = last.address !== undefined && last.received > 0 && sameAddress(last.address, address);\n return { address, outcome: reached ? \"reached\" : \"stopped\", cycles, hops };\n}\n\n/** This function makes the probe `path.mtr`: many cycles of probes on the path to one IP address with the tool mtr. */\nexport function mtrProbe(host: ToolHost): Probe<MtrInput, ToolRaw> {\n return defineProbe<MtrInput, ToolRaw>({\n id: \"path.mtr\",\n version: \"1\",\n summary: \"The loss and the round-trip times of each hop of the path to one IP address, from the tool mtr.\",\n layer: \"path\",\n mode: \"active\",\n kinds: [PATH_REPORT],\n tool: \"mtr\",\n peers: (input): readonly Target[] => [target(checked(input).address)],\n available: (context) => mtrAvailability(host, context),\n async run(input, context) {\n const command = mtrCommand(host.platform, input);\n if (command === undefined) throw new Error(`netdiag has no command of mtr for ${host.platform}.`);\n return runTool(host, command, context);\n },\n parse: (raw, input): readonly Observation[] => [observe(PATH_REPORT, target(input.address), readMtr(raw, input), { start: raw.start, end: raw.end })],\n });\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/netdiag",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "description": "Network diagnosis: typed facts at each layer of the stack, probes as adapters, and a scope rule for active probes.",
6
6
  "keywords": [
7
7
  "network",
@@ -33,10 +33,18 @@
33
33
  "types": "./build/index.d.ts",
34
34
  "default": "./build/index.js"
35
35
  },
36
+ "./address": {
37
+ "types": "./build/address.d.ts",
38
+ "default": "./build/address.js"
39
+ },
36
40
  "./diagnosis": {
37
41
  "types": "./build/diagnosis.d.ts",
38
42
  "default": "./build/diagnosis.js"
39
43
  },
44
+ "./tools": {
45
+ "types": "./build/tools.d.ts",
46
+ "default": "./build/tools.js"
47
+ },
40
48
  "./package.json": "./package.json"
41
49
  },
42
50
  "files": [
@@ -52,16 +60,20 @@
52
60
  },
53
61
  "dependencies": {
54
62
  "@mark1russell7/algebra": "^0.8.8",
55
- "@mark1russell7/optional": "^1.2.0"
63
+ "@mark1russell7/optional": "^1.2.0",
64
+ "@mark1russell7/rigor-statistics": "^0.2.2"
56
65
  },
57
66
  "devDependencies": {
58
67
  "@mark1russell7/cue": "^2.0.0",
68
+ "@mark1russell7/rigor-chaos": "^0.2.2",
69
+ "@mark1russell7/rigor-proxy": "^0.2.1",
70
+ "@netdiag/fixtures": "0.1.0",
59
71
  "@types/node": "^26.6.5",
60
72
  "@vitest/coverage-v8": "^5.0.3",
61
- "typescript": "^7.0.2",
62
- "vitest": "^5.0.3",
73
+ "fast-check": "^4.10.2",
63
74
  "tsdown": "^0.23.7",
64
- "fast-check": "^4.10.2"
75
+ "typescript": "^7.0.2",
76
+ "vitest": "^5.0.3"
65
77
  },
66
78
  "scripts": {
67
79
  "build": "tsdown",
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The IP addresses and the networks of netdiag: the family and the class of an address, CIDR ranges and sets of ranges.
3
+ *
4
+ * This entry uses only the module `node:net`. An IPv4 address in IPv6 form (`::ffff:192.0.2.1`) is its IPv4 address.
5
+ */
6
+ import { BlockList, isIP } from "node:net";
7
+ import { err, ok, type Result } from "@mark1russell7/optional";
8
+
9
+ /** The family of an IP address. */
10
+ export type AddressFamily = "ipv4" | "ipv6";
11
+
12
+ /**
13
+ * The class of an IP address, from the special-purpose ranges of RFC 6890.
14
+ *
15
+ * - `unspecified`: `0.0.0.0/8` and `::`.
16
+ * - `loopback`: `127.0.0.0/8` and `::1`.
17
+ * - `private`: the ranges of RFC 1918.
18
+ * - `shared`: `100.64.0.0/10`, the carrier-grade NAT range of RFC 6598.
19
+ * - `link-local`: `169.254.0.0/16` and `fe80::/10`.
20
+ * - `unique-local`: `fc00::/7`, the private range of IPv6 (RFC 4193).
21
+ * - `documentation`: the ranges of RFC 5737 and RFC 3849 for examples.
22
+ * - `benchmarking`: `198.18.0.0/15` (RFC 2544).
23
+ * - `multicast`: `224.0.0.0/4` and `ff00::/8`.
24
+ * - `reserved`: `240.0.0.0/4`, with the broadcast address.
25
+ * - `public`: each other address.
26
+ */
27
+ export type AddressClass =
28
+ | "unspecified"
29
+ | "loopback"
30
+ | "private"
31
+ | "shared"
32
+ | "link-local"
33
+ | "unique-local"
34
+ | "documentation"
35
+ | "benchmarking"
36
+ | "multicast"
37
+ | "reserved"
38
+ | "public";
39
+
40
+ /** A CIDR range: an address of the network and the length of its prefix. */
41
+ export interface Cidr {
42
+ readonly address: string;
43
+ readonly prefix: number;
44
+ readonly family: AddressFamily;
45
+ }
46
+
47
+ /** The special-purpose ranges, as data. An address that is in no range is `public`. */
48
+ export const ADDRESS_CLASSES: readonly { readonly cidr: string; readonly class: Exclude<AddressClass, "public"> }[] = [
49
+ { cidr: "0.0.0.0/8", class: "unspecified" },
50
+ { cidr: "10.0.0.0/8", class: "private" },
51
+ { cidr: "100.64.0.0/10", class: "shared" },
52
+ { cidr: "127.0.0.0/8", class: "loopback" },
53
+ { cidr: "169.254.0.0/16", class: "link-local" },
54
+ { cidr: "172.16.0.0/12", class: "private" },
55
+ { cidr: "192.0.2.0/24", class: "documentation" },
56
+ { cidr: "192.168.0.0/16", class: "private" },
57
+ { cidr: "198.18.0.0/15", class: "benchmarking" },
58
+ { cidr: "198.51.100.0/24", class: "documentation" },
59
+ { cidr: "203.0.113.0/24", class: "documentation" },
60
+ { cidr: "224.0.0.0/4", class: "multicast" },
61
+ { cidr: "240.0.0.0/4", class: "reserved" },
62
+ { cidr: "::/128", class: "unspecified" },
63
+ { cidr: "::1/128", class: "loopback" },
64
+ { cidr: "2001:db8::/32", class: "documentation" },
65
+ { cidr: "fc00::/7", class: "unique-local" },
66
+ { cidr: "fe80::/10", class: "link-local" },
67
+ { cidr: "ff00::/8", class: "multicast" },
68
+ ];
69
+
70
+ /** The classes of the addresses that the internet does not route to a host. */
71
+ const NOT_ROUTED: ReadonlySet<AddressClass> = new Set(["unspecified", "loopback", "private", "shared", "link-local", "unique-local"]);
72
+
73
+ /** This function gives the canonical form of an address: no spaces, no brackets, lower case, and IPv4 for a mapped IPv4 address. */
74
+ export function canonicalAddress(address: string): string {
75
+ let text = address.trim().toLowerCase();
76
+ if (text.startsWith("[") && text.endsWith("]")) text = text.slice(1, -1);
77
+ const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(text)?.[1];
78
+ return mapped ?? text;
79
+ }
80
+
81
+ /** This function gives the family of an IP address, or `undefined` for a text that is not an IP address. */
82
+ export function addressFamily(address: string): AddressFamily | undefined {
83
+ const family = isIP(canonicalAddress(address));
84
+ return family === 4 ? "ipv4" : family === 6 ? "ipv6" : undefined;
85
+ }
86
+
87
+ /** This function gives the CIDR range of a text, for example `192.0.2.0/24` or `2001:db8::/32`. An address alone is a range of one address. */
88
+ export function parseCidr(text: string): Result<Cidr, string> {
89
+ const slash = text.indexOf("/");
90
+ const address = canonicalAddress(slash === -1 ? text : text.slice(0, slash));
91
+ const family = addressFamily(address);
92
+ if (family === undefined) return err(`"${text}" is not an IP address or a CIDR range.`);
93
+ const max = family === "ipv4" ? 32 : 128;
94
+ const prefix = slash === -1 ? String(max) : text.slice(slash + 1).trim();
95
+ if (!/^\d{1,3}$/.test(prefix) || Number(prefix) > max) return err(`"${text}" has no prefix from 0 to ${max}.`);
96
+ return ok({ address, prefix: Number(prefix), family });
97
+ }
98
+
99
+ /** This function gives the text of a CIDR range. */
100
+ export function formatCidr(cidr: Cidr): string {
101
+ return `${cidr.address}/${cidr.prefix}`;
102
+ }
103
+
104
+ /** A set of CIDR ranges. */
105
+ export interface CidrSet {
106
+ readonly ranges: readonly Cidr[];
107
+ /** This function tells if an address is in a range of the set. A text that is not an IP address is in no set. */
108
+ has(address: string): boolean;
109
+ }
110
+
111
+ /** This function gives the set of some CIDR ranges. */
112
+ export function cidrSet(ranges: readonly Cidr[]): CidrSet {
113
+ const list = new BlockList();
114
+ for (const range of ranges) list.addSubnet(range.address, range.prefix, range.family);
115
+ return {
116
+ ranges: [...ranges],
117
+ has(address) {
118
+ const host = canonicalAddress(address);
119
+ const family = addressFamily(host);
120
+ return family !== undefined && list.check(host, family);
121
+ },
122
+ };
123
+ }
124
+
125
+ /** This function tells if an address is in a CIDR range. */
126
+ export function cidrContains(cidr: Cidr, address: string): boolean {
127
+ return cidrSet([cidr]).has(address);
128
+ }
129
+
130
+ let classSets: readonly { readonly class: Exclude<AddressClass, "public">; readonly set: CidrSet }[] | undefined;
131
+
132
+ function classes(): NonNullable<typeof classSets> {
133
+ classSets ??= ADDRESS_CLASSES.map((entry) => {
134
+ const range = parseCidr(entry.cidr);
135
+ if (range.tag === "err") throw new Error(range.error);
136
+ return { class: entry.class, set: cidrSet([range.value]) };
137
+ });
138
+ return classSets;
139
+ }
140
+
141
+ /** This function gives the class of an IP address, or `undefined` for a text that is not an IP address. */
142
+ export function addressClass(address: string): AddressClass | undefined {
143
+ if (addressFamily(address) === undefined) return undefined;
144
+ return classes().find((entry) => entry.set.has(address))?.class ?? "public";
145
+ }
146
+
147
+ /**
148
+ * This function tells if the internet routes to an address. A `private` address is unspecified, loopback, private,
149
+ * shared, link-local or unique-local. Each other address is `public`, also an address for documentation.
150
+ */
151
+ export function addressKind(address: string): "public" | "private" | undefined {
152
+ const found = addressClass(address);
153
+ if (found === undefined) return undefined;
154
+ return NOT_ROUTED.has(found) ? "private" : "public";
155
+ }
@@ -38,14 +38,14 @@ export const STANDARD_FEATURES: readonly Feature[] = [
38
38
  {
39
39
  id: "dns.answer",
40
40
  layer: "dns",
41
- summary: "The result of the DNS lookup of the name of the target.",
42
- values: ["ok", "nxdomain", "servfail", "refused", "timeout"],
43
- healthy: { ok: 0.97, nxdomain: 0.01, servfail: 0.005, refused: 0.005, timeout: 0.01 },
41
+ summary: "The result of the DNS lookup of the name of the target. The value unreachable tells that no DNS service listens on the resolver.",
42
+ values: ["ok", "nxdomain", "servfail", "refused", "timeout", "unreachable"],
43
+ healthy: { ok: 0.965, nxdomain: 0.01, servfail: 0.005, refused: 0.005, timeout: 0.01, unreachable: 0.005 },
44
44
  },
45
45
  {
46
46
  id: "dns.address",
47
47
  layer: "dns",
48
- summary: "The kind of the addresses of a public name: public, or private (RFC 1918, RFC 4193 or loopback).",
48
+ summary: "The kind of the addresses of a public name: public, or private (RFC 1918, RFC 6598, RFC 4193, loopback, link-local or unspecified).",
49
49
  values: ["public", "private"],
50
50
  healthy: { public: 0.95, private: 0.05 },
51
51
  },
@@ -87,7 +87,7 @@ export const STANDARD_FEATURES: readonly Feature[] = [
87
87
  {
88
88
  id: "tls.ratio",
89
89
  layer: "tls",
90
- summary: "The time of the TLS handshake divided by the time of the TCP connect: low (below 3), mid, or high (above 8).",
90
+ summary: "The time of the TLS handshake divided by the time of the TCP connect: low (below 3), mid, or high (8 or more).",
91
91
  values: ["low", "mid", "high"],
92
92
  healthy: { low: 0.75, mid: 0.2, high: 0.05 },
93
93
  },
@@ -108,7 +108,7 @@ export const STANDARD_FEATURES: readonly Feature[] = [
108
108
  {
109
109
  id: "http.ttfb",
110
110
  layer: "http",
111
- summary: "The time to the first byte divided by the time of the TCP connect: low (below 4), mid, or high (above 20).",
111
+ summary: "The time to the first byte divided by the time of the TCP connect: low (below 4), mid, or high (20 or more).",
112
112
  values: ["low", "mid", "high"],
113
113
  healthy: { low: 0.6, mid: 0.3, high: 0.1 },
114
114
  },
@@ -177,7 +177,7 @@ export const STANDARD_FAULTS: readonly Fault[] = [
177
177
  summary: "The DNS resolver does not answer.",
178
178
  step: "Use a different DNS resolver, or examine the resolver of the router.",
179
179
  predicts: {
180
- "dns.answer": { expect: "timeout", strength: "strong" },
180
+ "dns.answer": { expect: ["timeout", "unreachable"], strength: "strong" },
181
181
  },
182
182
  },
183
183
  {
@@ -344,7 +344,7 @@ export const STANDARD_FAULTS: readonly Fault[] = [
344
344
 
345
345
  /** The standard catalog: the rules of practice as the first likelihoods. Each hypothesis has the prior weight 1. */
346
346
  export const STANDARD_CATALOG_SPEC: CatalogSpec = {
347
- version: "1",
347
+ version: "2",
348
348
  features: STANDARD_FEATURES,
349
349
  faults: STANDARD_FAULTS,
350
350
  };
@@ -31,4 +31,17 @@ export {
31
31
  type Strength,
32
32
  } from "./model.ts";
33
33
  export { STANDARD_CATALOG, STANDARD_CATALOG_SPEC, STANDARD_FAULTS, STANDARD_FEATURES } from "./catalog.ts";
34
+ export {
35
+ LAB_RECORD_FORMAT,
36
+ labGate,
37
+ labRecordOf,
38
+ scoreLab,
39
+ type LabBars,
40
+ type LabCause,
41
+ type LabGate,
42
+ type LabRecord,
43
+ type LabScore,
44
+ type LabStatus,
45
+ type LabVerdict,
46
+ } from "./score.ts";
34
47
  export { LAYERS, isLayer, layerRank, type Layer } from "../layer.ts";
@@ -0,0 +1,160 @@
1
+ import { err, ok, type Result } from "@mark1russell7/optional";
2
+ import { type ClassScore, type Confusion, type Proportion, brier, classScores, confusion, wilson } from "@mark1russell7/rigor-statistics";
3
+ import type { Catalog, Diagnosis } from "./model.ts";
4
+
5
+ /** The format of a lab record. */
6
+ export const LAB_RECORD_FORMAT = "netdiag.lab-record/1";
7
+
8
+ /** The probability that a diagnosis gives to one cause. */
9
+ export interface LabCause {
10
+ readonly id: string;
11
+ readonly probability: number;
12
+ }
13
+
14
+ /** One run of a lab case: the hypothesis that the lab made true, and the diagnosis of netdiag. */
15
+ export interface LabRecord {
16
+ readonly format: typeof LAB_RECORD_FORMAT;
17
+ /** The id of the case, for example `tls.cert-invalid/expired`. */
18
+ readonly case: string;
19
+ /** The version of the catalog that gave the diagnosis. */
20
+ readonly catalog: string;
21
+ /** The hypothesis that the lab made true: a fault of the catalog, `none` or `unknown`. */
22
+ readonly truth: string;
23
+ /** The most probable cause of the diagnosis. */
24
+ readonly predicted: string;
25
+ /** The rank of the truth in the causes, from 1. */
26
+ readonly rank: number;
27
+ /** The probability of each cause, from the most probable. */
28
+ readonly causes: readonly LabCause[];
29
+ }
30
+
31
+ /**
32
+ * This function makes the record of one run of a lab case. The truth must be a hypothesis of the catalog, and the
33
+ * diagnosis must have a cause.
34
+ */
35
+ export function labRecordOf(catalog: Catalog, caseId: string, truth: string, diagnosis: Diagnosis): Result<LabRecord, string> {
36
+ if (!catalog.hypotheses.some((hypothesis) => hypothesis.id === truth)) return err(`The truth "${truth}" is not a hypothesis of the catalog ${catalog.version}.`);
37
+ const first = diagnosis.causes[0];
38
+ if (first === undefined) return err("The diagnosis has no cause.");
39
+ const index = diagnosis.causes.findIndex((cause) => cause.id === truth);
40
+ if (index < 0) return err(`The diagnosis has no cause "${truth}".`);
41
+ return ok({
42
+ format: LAB_RECORD_FORMAT,
43
+ case: caseId,
44
+ catalog: catalog.version,
45
+ truth,
46
+ predicted: first.id,
47
+ rank: index + 1,
48
+ causes: diagnosis.causes.map((cause) => ({ id: cause.id, probability: cause.probability })),
49
+ });
50
+ }
51
+
52
+ /** The scores of the lab records of one catalog. */
53
+ export interface LabScore {
54
+ /** The version of the catalog. */
55
+ readonly catalog: string;
56
+ /** The number of records. */
57
+ readonly n: number;
58
+ /** The number of records where the predicted cause is the truth. */
59
+ readonly correct: number;
60
+ /** The Wilson interval of the part of the records where the predicted cause is the truth. */
61
+ readonly accuracy: Proportion;
62
+ /** The confusion table. It has a row for each hypothesis of the catalog, in the order of the catalog. */
63
+ readonly table: Confusion<string>;
64
+ /** The scores of each hypothesis, in the order of the catalog. */
65
+ readonly classes: readonly ClassScore<string>[];
66
+ /** The mean Brier score of the records: the squared distance of the probabilities from the truth, from 0 to 2. */
67
+ readonly brier: number | undefined;
68
+ }
69
+
70
+ /**
71
+ * This function scores the lab records of one catalog. The recall of a hypothesis is the part of its runs where the
72
+ * diagnosis names it. A record of a different catalog version, or with a hypothesis that is not in the catalog, is an error.
73
+ */
74
+ export function scoreLab(catalog: Catalog, records: readonly LabRecord[]): Result<LabScore, string[]> {
75
+ const ids = catalog.hypotheses.map((hypothesis) => hypothesis.id);
76
+ const known = new Set(ids);
77
+ const problems: string[] = [];
78
+ records.forEach((record, i) => {
79
+ const name = `Record ${i} (${record.case})`;
80
+ if (record.format !== LAB_RECORD_FORMAT) problems.push(`${name} has the format "${String(record.format)}", not "${LAB_RECORD_FORMAT}".`);
81
+ if (record.catalog !== catalog.version) problems.push(`${name} comes from the catalog ${record.catalog}, not ${catalog.version}.`);
82
+ for (const id of new Set([record.truth, record.predicted, ...record.causes.map((cause) => cause.id)])) {
83
+ if (!known.has(id)) problems.push(`${name} names "${id}", which is not a hypothesis of the catalog.`);
84
+ }
85
+ });
86
+ if (problems.length > 0) return err(problems);
87
+ const table = confusion(
88
+ records.map((record) => record.truth),
89
+ records.map((record) => record.predicted),
90
+ ids,
91
+ );
92
+ const forecasts = records.flatMap((record) => {
93
+ const probability = new Map(record.causes.map((cause) => [cause.id, cause.probability]));
94
+ return ids.map((id) => ({ p: probability.get(id) ?? 0, hit: id === record.truth }));
95
+ });
96
+ return ok({
97
+ catalog: catalog.version,
98
+ n: table.n,
99
+ correct: table.correct,
100
+ accuracy: wilson(table.correct, table.n),
101
+ table,
102
+ classes: classScores(table),
103
+ brier: records.length === 0 ? undefined : brier(forecasts) * ids.length,
104
+ });
105
+ }
106
+
107
+ /** The least lower end of the Wilson interval of the recall, for each hypothesis that the gate holds. */
108
+ export type LabBars = Readonly<Record<string, number>>;
109
+
110
+ /** The verdict of the gate on one hypothesis: `no-read` when no record has it as the truth. */
111
+ export type LabStatus = "pass" | "fail" | "no-read";
112
+
113
+ /** The verdict of the gate on one hypothesis. */
114
+ export interface LabVerdict {
115
+ readonly hypothesis: string;
116
+ readonly status: LabStatus;
117
+ /** The bar of the hypothesis. */
118
+ readonly bar: number;
119
+ /** The lower end of the recall interval. It is `undefined` when no record has the hypothesis as the truth. */
120
+ readonly low: number | undefined;
121
+ /** The number of records with the hypothesis as the truth. */
122
+ readonly support: number;
123
+ }
124
+
125
+ /** The result of the accuracy gate. */
126
+ export interface LabGate {
127
+ /** It is `true` only when each verdict is `pass`. A hypothesis with no read does not pass. */
128
+ readonly pass: boolean;
129
+ /** The verdicts, in the order of the catalog. */
130
+ readonly verdicts: readonly LabVerdict[];
131
+ /** The hypotheses with no bar, for example a fault that no lab can make. */
132
+ readonly unbarred: readonly string[];
133
+ }
134
+
135
+ /**
136
+ * This function holds the recall of each hypothesis with a bar. A hypothesis passes when the lower end of its
137
+ * recall interval is the bar or more. A bar for a hypothesis that is not in the score is an error.
138
+ */
139
+ export function labGate(score: LabScore, bars: LabBars): Result<LabGate, string[]> {
140
+ const problems: string[] = [];
141
+ const labels = new Set(score.table.labels);
142
+ for (const [hypothesis, bar] of Object.entries(bars)) {
143
+ if (!labels.has(hypothesis)) problems.push(`The bar of "${hypothesis}" is not for a hypothesis of the catalog ${score.catalog}.`);
144
+ if (!(bar >= 0 && bar <= 1)) problems.push(`The bar of "${hypothesis}" is ${bar}. A bar is from 0 to 1.`);
145
+ }
146
+ if (problems.length > 0) return err(problems);
147
+ const verdicts: LabVerdict[] = [];
148
+ const unbarred: string[] = [];
149
+ for (const scores of score.classes) {
150
+ const bar = bars[scores.label];
151
+ if (bar === undefined) {
152
+ unbarred.push(scores.label);
153
+ continue;
154
+ }
155
+ const low = scores.support === 0 ? undefined : scores.recallInterval.low;
156
+ const status: LabStatus = low === undefined ? "no-read" : low >= bar ? "pass" : "fail";
157
+ verdicts.push({ hypothesis: scores.label, status, bar, low, support: scores.support });
158
+ }
159
+ return ok({ pass: verdicts.length > 0 && verdicts.every((verdict) => verdict.status === "pass"), verdicts, unbarred });
160
+ }
package/src/index.ts CHANGED
@@ -30,4 +30,39 @@ export {
30
30
  type ScopeDecision,
31
31
  } from "./scope.ts";
32
32
  export { createRunner, type ClockOrigin, type ProbeOutcome, type Runner, type RunnerOptions } from "./run.ts";
33
+ export * from "./address/index.ts";
33
34
  export * from "./diagnosis/index.ts";
35
+ export * from "./kinds.ts";
36
+ export {
37
+ DEFAULT_MAX_BYTES,
38
+ DNS_PROBE,
39
+ HTTP_PROBE,
40
+ TCP_PROBE,
41
+ TLS_PROBE,
42
+ dnsOutcome,
43
+ httpProbe,
44
+ tcpProbe,
45
+ tlsProbe,
46
+ type DnsInput,
47
+ type DnsRaw,
48
+ type HttpInput,
49
+ type HttpRaw,
50
+ type TcpInput,
51
+ type TcpRaw,
52
+ type TlsInput,
53
+ type TlsProbeOptions,
54
+ type TlsRaw,
55
+ } from "./probes/native.ts";
56
+ export {
57
+ DEFAULT_TIMEOUT_MS,
58
+ KEPT_FIELDS,
59
+ certProblem,
60
+ connectOutcome,
61
+ handshakeOutcome,
62
+ type ConnectOptions,
63
+ type Dial,
64
+ type HttpPhase,
65
+ type TcpPhase,
66
+ type TlsPhase,
67
+ } from "./probes/socket.ts";
68
+ export * from "./readers.ts";