Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Re-include the parent directory and its contents.

Because tools/ is ignored, the negation for tools/linux-diag/ does not make tools/linux-diag/lzt-netdiag or README.md trackable. Use parent/content exception rules, for example:

+!tools/
+tools/*
 !tools/linux-diag/
+!tools/linux-diag/**

Verify with git check-ignore -v tools/linux-diag/lzt-netdiag tools/linux-diag/README.md.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
!tools/linux-diag/ # Linux network diagnostics dispatcher (lzt-netdiag) - COMMITTED
!tools/
tools/*
!tools/linux-diag/
!tools/linux-diag/**
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore at line 61, Update the .gitignore rules for tools/linux-diag so
the ignored tools/ parent directory and its contents are explicitly re-included,
allowing lzt-netdiag and README.md to be tracked. Preserve the intended
exclusion of unrelated tools entries, and verify both files with git
check-ignore -v.

docs/assets/screenshots/ # Auto-generated screenshots (upload to VPS instead)
playwright/ # Browser automation cache
.playwright/
Expand Down
115 changes: 115 additions & 0 deletions tools/linux-diag/README.md
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +46 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Create the install destination first.

install -m does not create ~/.local/bin; setup fails on hosts where that directory is absent. Add mkdir -p ~/.local/bin or use install -Dm0755.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/linux-diag/README.md` around lines 47 - 48, Update the installation
instructions near the lzt-netdiag install command to create ~/.local/bin before
installing, using either mkdir -p or install’s directory-creation option, while
preserving the existing 0755 permissions and destination.

```

## 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
177 changes: 177 additions & 0 deletions tools/linux-diag/lzt-netdiag
Original file line number Diff line number Diff line change
@@ -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/
Comment thread
coderabbitai[bot] marked this conversation as resolved.
#
# 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
}
Comment on lines +56 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align the missing-tool exit-code contract.

  • tools/linux-diag/lzt-netdiag#L57-L60: return 127 for missing exported tools, or change the documented status.
  • tools/linux-diag/lzt-netdiag#L84-L88: make check_tool() use the selected status consistently.
  • tools/linux-diag/README.md#L82-L89: document the same status exposed by the script.
📍 Affects 2 files
  • tools/linux-diag/lzt-netdiag#L57-L60 (this comment)
  • tools/linux-diag/lzt-netdiag#L84-L88
  • tools/linux-diag/README.md#L82-L89
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/linux-diag/lzt-netdiag` around lines 57 - 60, Align the missing-tool
exit status across the diagnostic script and documentation: update die() in
tools/linux-diag/lzt-netdiag (57-60) to use the selected status, make
check_tool() in tools/linux-diag/lzt-netdiag (84-88) consistently return that
status, and document the same status in tools/linux-diag/README.md (82-89).


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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

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[@]}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# 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
Loading