diff --git a/.gitignore b/.gitignore index be11ee2..e396296 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ network_failover.log # Development tools (NOT for end users - gitignored) tools/ # Dev scripts (screenshot gen, favicon gen, etc.) +!tools/linux-diag/ # Linux network diagnostics dispatcher (lzt-netdiag) - COMMITTED docs/assets/screenshots/ # Auto-generated screenshots (upload to VPS instead) playwright/ # Browser automation cache .playwright/ diff --git a/tools/linux-diag/README.md b/tools/linux-diag/README.md new file mode 100644 index 0000000..4d9aaf1 --- /dev/null +++ b/tools/linux-diag/README.md @@ -0,0 +1,115 @@ +# linux-diag — Network diagnostics dispatcher + +This directory hosts `lzt-netdiag`, a thin bash dispatcher over four network +diagnostic tools that are not packaged for Debian Forky upstream +(`speedtest-cli`, `iperf3`, `mtr`, `traceroute`) — instead, they run inside an +Arch Linux **distrobox** called `arch-loust` and are exported back to the host +PATH via `distrobox-export --bin`. + +## Why a dispatcher instead of calling the wrappers directly? + +| Concern | Without dispatcher | With dispatcher | +|---|---|---| +| Memory | `distrobox enter arch-loust -- speedtest-cli --simple` | `lzt-netdiag st --simple` | +| Tab-completion | 4 distinct command names | 1 canonical name with 6 subcommands | +| Auto-detection of local NICs | Manual `iw dev`, `ip route` | `lzt-netdiag host` | +| Tool availability | Each wrapper is independent | Pre-flight check fails fast with actionable error | + +## Subcommands + +| Subcommand | Pass-through to | Notes | +|---|---|---| +| `st` | `speedtest-cli` | Accepts all native flags (`--simple`, `--json`, `--list`, `--server N`, etc.) | +| `i3` | `iperf3` | Accepts all native flags (`-c`, `-u`, `-t`, `-j`, `-b`, etc.) | +| `mtr` | `mtr` | Accepts all native flags (`-c`, `-w`, `-J`, etc.) | +| `trace` | `traceroute` | Accepts all native flags | +| `host` | host sysfs + iproute2 | Local-only nic auto-detect (interfaces, default route, wireless quality, DNS resolvers, gateway ping) | +| `version` | all four binaries | One-liner with versions + container name | +| `-h`, `--help`, `help`, no args | — | Extracted from script doc block via sed | + +## Setup (one-time) + +```sh +# 1. Create the distrobox (Arch Linux base; speedtest-cli, iperf3, mtr, +# traceroute are in upstream Arch repos). +distrobox create -i archlinux:latest -n arch-loust +distrobox enter arch-loust -- sudo pacman -S speedtest-cli iperf3 mtr traceroute + +# 2. Export each binary to the host PATH +distrobox enter arch-loust -- distrobox-export \ + --bin /usr/bin/speedtest-cli \ + --bin /usr/bin/iperf3 \ + --bin /usr/bin/mtr \ + --bin /usr/bin/traceroute \ + --export-path ~/.local/bin/ + +# 3. Drop this directory on the host +install -m 0755 lzt-netdiag ~/.local/bin/lzt-netdiag +``` + +## Usage examples + +```sh +# Bandwidth + latency in compact form +lzt-netdiag st --simple + +# Raw JSON for scripting +lzt-netdiag st --json | jq '.download.bandwidth, .ping' + +# Bandwidth to a specific server for N seconds +lzt-netdiag i3 -c 192.168.1.64 -t 30 + +# UDP stream at 1 Mb/s for 5 seconds +lzt-netdiag i3 -c 192.168.1.64 -t 5 -u -b 1M + +# Path MTU / packet-loss diagnostic +lzt-netdiag mtr 192.168.1.64 -c 30 -w + +# JSON mtr for log ingest +lzt-netdiag mtr -J 192.168.1.64 + +# What hop is the bottleneck? +lzt-netdiag trace 1.1.1.1 + +# Local NIC inventory + gateway reachability +lzt-netdiag host + +# Confirm all 4 binaries are exported and which container they're in +lzt-netdiag version +``` + +## Exit codes + +| Code | Meaning | +|---|---| +| `0` | OK | +| `1` | Argument / tool / container error (with stderr message) | +| `127` | Tool missing from host PATH (re-run `distrobox-export`) | +| Other | Pass-through from underlying tool (e.g. `mtr` returns its own codes) | + +## Why is this in NetBoozt? + +The NetBoozt platform targets Windows users (see `runner-strategy.md`), but the +**build & maintain** workflow runs on Debian. When validating a release that +claims e.g. "improved connection-resilience on lossy networks", we need to +reproduce typical home-network pathologies — packet loss, MTU black holes, +slow DNS, asymmetric bandwidth — without polluting the host distro with +Arch-only packages. The dispatcher pattern lets the maintainer reproduce +field issues deterministically while keeping the host clean. + +## Why a single-file script? + +- Zero dependencies beyond `bash`, `distrobox`, `iproute2`, `procps`. +- Trivial to audit (160 lines). +- Single source of truth — `lzt-netdiag --help` reads the same doc-block that + this README documents, so they cannot drift. + +## Customization + +The container name is configurable via `LZT_ARCH_LOUST` env var if you need +multiple coexisting arch distroboxes (e.g. `LZT_ARCH_LOUST=arch-test`). + +## See also + +- `runner-strategy.md` — Why builds use `windows-latest` (VPS runner is Linux-only) +- `tools/` — Other helper scripts committed to NetBoozt diff --git a/tools/linux-diag/lzt-netdiag b/tools/linux-diag/lzt-netdiag new file mode 100755 index 0000000..454b0e2 --- /dev/null +++ b/tools/linux-diag/lzt-netdiag @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +# lzt-netdiag — Network diagnostics dispatcher. +# +# Thin layer over speedtest-cli, iperf3, mtr and traceroute exported +# from the 'arch-loust' Arch Linux distrobox. Each subcommand is a +# pass-through to the underlying tool with its full flag surface; the +# only extras are 'host' (local NIC auto-detect) and sensible bash +# helpers (exit-code-friendly, --help via sed-extract). +# +# Why a dispatcher instead of calling the wrappers directly? +# - One short command (`lzt-netdiag st --simple`) instead of writing +# `distrobox enter arch-loust -- speedtest-cli --simple` every time. +# - Tab-completion target (single canonical name → can hook bash-completion). +# - Local-only affordances that the upstream tools don't have: +# * `host` → auto-detect Wi-Fi vs Ethernet interface + default route. +# * `version` → one-liner showing versions of all 4 tools. +# +# Setup (one-time; only needed if exporting after re-install): +# distrobox create -i archlinux:latest -n arch-loust +# distrobox enter arch-loust -- sudo pacman -S speedtest-cli iperf3 mtr traceroute +# distrobox enter arch-loust -- distrobox-export \ +# --bin /usr/bin/speedtest-cli \ +# --bin /usr/bin/iperf3 \ +# --bin /usr/bin/mtr \ +# --bin /usr/bin/traceroute \ +# --export-path ~/.local/bin/ +# +# Usage: +# lzt-netdiag st [--simple | --json | --list | --server N] # speedtest-cli +# lzt-netdiag i3 [-c host] [-u] [-t sec] [-j] # iperf3 +# lzt-netdiag mtr [-c N] [-w] [-J] host # mtr +# lzt-netdiag trace host # traceroute +# lzt-netdiag host # local NIC info +# lzt-netdiag version # all 4 versions +# lzt-netdiag (-h | --help | help) +# +# Examples: +# lzt-netdiag st --simple +# lzt-netdiag st --json | jq '.download.bandwidth, .ping' +# lzt-netdiag i3 -c 192.168.1.64 -t 30 +# lzt-netdiag i3 -c 192.168.1.64 -t 5 -u -b 1M # UDP 1 Mb/s, 5s +# lzt-netdiag mtr 192.168.1.64 -c 30 -w # wide report +# lzt-netdiag mtr -J 192.168.1.64 # JSON report +# lzt-netdiag host +# +# Exit codes: 0=OK, 1=argument/tool/container error, +# 127=tool missing in PATH (re-export from arch-loust). +# +# Source of truth: this file is the canonical dispatcher. If you need +# to extend, add a new case below and update the usage block above. + +set -euo pipefail + +CONTAINER_NAME="${LZT_ARCH_LOUST:-arch-loust}" + +die() { + echo "lzt-netdiag: $*" >&2 + exit 1 +} + +die_missing_tool() { + echo "lzt-netdiag: $*" >&2 + # 127 matches bash convention for "command not found". + exit 127 +} + +usage() { + # Extract the doc block between the second '#' line and the first + # blank line. Robust against edits to header. + sed -n '3,/^[^#]/p' "$0" \ + | sed -n '1,/^$/p' \ + | sed 's/^# \{0,1\}//' + exit 0 +} + +ensure_container_running() { + command -v distrobox >/dev/null || die "distrobox not found in PATH" + # Filter distrobox list output to our specific container; avoid the + # "any container is Up" shortcut which silently passes when another + # distrobox is running but ours is not. + local row + if ! row="$(distrobox list 2>/dev/null | grep -E "(^|[[:space:]])${CONTAINER_NAME}([[:space:]]|$)")"; then + die "container '${CONTAINER_NAME}' not present. Create it: distrobox create -i archlinux:latest -n ${CONTAINER_NAME}" + fi + # Status column varies between distrobox versions (1.4.x: 3rd col; + # 1.5+: 4th col with STATUS first). Use word-anchored match. + if ! echo "$row" | grep -wq "Up"; then + die "container '${CONTAINER_NAME}' is not Up. Start it: distrobox start ${CONTAINER_NAME}" + fi +} + +check_tool() { + local tool="$1" + command -v "$tool" >/dev/null 2>&1 || \ + die_missing_tool "tool '${tool}' not exported on host. Re-run: distrobox enter ${CONTAINER_NAME} -- distrobox-export --bin /usr/bin/${tool} --export-path ~/.local/bin/" +} + +# Dispatch happens in the case below; preflight (container + tool check) +# runs ONLY for subcommands that actually need the distrobox exports +# (st, i3, mtr, trace). Help, host, and version work without them. + +cmd="${1:-help}" +shift || true + +case "$cmd" in + st) + ensure_container_running; check_tool speedtest-cli + exec speedtest-cli "$@" + ;; + i3) + ensure_container_running; check_tool iperf3 + exec iperf3 "$@" + ;; + mtr) + ensure_container_running; check_tool mtr + exec mtr "$@" + ;; + trace) + ensure_container_running; check_tool traceroute + exec traceroute "$@" + ;; + host) + # Local-only nic auto-detect. Uses host sysfs + iproute2. + printf "=== Interfaces ===\n" + ip -br addr show up 2>/dev/null \ + || echo "(iproute2 not on host: ip -br not found)" + printf "\n=== Default route ===\n" + ip route show default 2>/dev/null \ + || echo "(no default route)" + printf "\n=== Wireless interfaces (sysfs) ===\n" + # Cannot use `local` outside a function; use a subshell + captured stdout. + wl_list="$(for iface in /sys/class/net/*; do + [ -d "$iface/wireless" ] && basename "$iface" + done)" + if [ -n "$wl_list" ]; then + # Use mapfile so every newline-separated interface is preserved + # (read -ra <<< would only consume the first line). + mapfile -t wl_arr <<< "$wl_list" + printf " %s\n" "${wl_arr[@]}" + # Link quality from /proc/net/wireless + if [ -r /proc/net/wireless ]; then + printf "\n=== /proc/net/wireless ===\n" + awk 'NR>2 {printf " %s\n", $0}' /proc/net/wireless + fi + else + echo " (none)" + fi + printf "\n=== DNS resolvers (/etc/resolv.conf) ===\n" + awk '/^nameserver/ {printf " %s\n", $2}' /etc/resolv.conf 2>/dev/null \ + || echo " (none / unreadable)" + printf "\n=== Gateway reachability (5 pings) ===\n" + gw="$(ip route show default 2>/dev/null | awk '/default/ {print $3; exit}')" + if [ -n "$gw" ]; then + if command -v ping >/dev/null 2>&1; then + ping -c 5 -W 2 "$gw" 2>&1 | tail -4 + else + echo " (ping not installed on host — install iputils-ping / inetutils-ping to enable)" + fi + else + echo " (no gateway detected)" + fi + ;; + version) + printf "speedtest-cli: " ; speedtest-cli --version 2>&1 | head -1 + printf "iperf3: " ; iperf3 --version 2>&1 | head -1 + printf "mtr: " ; mtr --version 2>&1 | head -1 + # traceroute writes version to stderr (modern 2.x); capture both + printf "traceroute: " ; traceroute --version 2>&1 | head -1 + printf "container: %s\n" "$CONTAINER_NAME" + ;; + -h|--help|help|"") + usage + ;; + *) + die "unknown subcommand '${cmd}'. Run: lzt-netdiag --help" + ;; +esac