diff --git a/README.md b/README.md index 0b499b4..f7911d3 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,19 @@ Stats are collected every 2 seconds in a background thread with minimal performa +## Screenshots + + + + + + + + + + +
Overview
Connections table with live stats and sparklines
Details
Per-connection SNI, cipher, GeoIP, DPI
Graph
Traffic chart, app distribution, top processes
Interfaces
Per-interface RX/TX history with errors and drops
+ ## Quick Start ### Installation diff --git a/assets/rustnet.gif b/assets/rustnet.gif index 61bbf92..b211771 100644 Binary files a/assets/rustnet.gif and b/assets/rustnet.gif differ diff --git a/assets/screenshots/details.png b/assets/screenshots/details.png new file mode 100644 index 0000000..0a595c7 Binary files /dev/null and b/assets/screenshots/details.png differ diff --git a/assets/screenshots/filter.png b/assets/screenshots/filter.png new file mode 100644 index 0000000..cd4c0b1 Binary files /dev/null and b/assets/screenshots/filter.png differ diff --git a/assets/screenshots/graph.png b/assets/screenshots/graph.png new file mode 100644 index 0000000..09a28c4 Binary files /dev/null and b/assets/screenshots/graph.png differ diff --git a/assets/screenshots/grouping.png b/assets/screenshots/grouping.png new file mode 100644 index 0000000..2af6643 Binary files /dev/null and b/assets/screenshots/grouping.png differ diff --git a/assets/screenshots/interfaces.png b/assets/screenshots/interfaces.png new file mode 100644 index 0000000..275bbe1 Binary files /dev/null and b/assets/screenshots/interfaces.png differ diff --git a/assets/screenshots/overview.png b/assets/screenshots/overview.png new file mode 100644 index 0000000..e7fdc47 Binary files /dev/null and b/assets/screenshots/overview.png differ diff --git a/demo.tape b/demo.tape new file mode 100644 index 0000000..ff6a503 --- /dev/null +++ b/demo.tape @@ -0,0 +1,86 @@ +# vhs tape for the rustnet showcase demo. +# +# Render with: scripts/record-rustnet-demo.sh +# (wraps `vhs demo.tape` with sudo pre-cache, background traffic, +# and gifsicle optimization). +# +# Manual render: vhs demo.tape (writes assets/rustnet.gif). +# Pre-build the release binary first: cargo build --release +# Capture needs root: the tape runs `sudo -A`, which reads the password +# from the askpass helper pointed to by $SUDO_ASKPASS. The wrapper script +# above sets that automatically. For manual runs, export SUDO_ASKPASS +# yourself (or on Linux set CAP_NET_RAW on the binary and drop the +# `sudo -A` from the tape). +# +# Storyboard (~30s, 7 beats): +# 1. Land on Overview, let traffic populate the table and sparkline. +# 2. Walk three rows so the right-side panels update (continuity). +# 3. Filter mode: /, type "443", briefly settle, Esc to clear. +# 4. Process grouping: a, Down, Space to expand a node, a to flatten. +# 5. Enter zooms into Details on the selected connection. +# 6. Esc back to Overview, Tab x3 to land on Graph. +# 7. Esc back to Overview to close the loop. +# +# Sized for the ratatui showcase: 1200x720 fits the 800-1200px guidance and +# stays readable on phone-width viewports. + +Output assets/rustnet.gif + +Set Theme "Catppuccin Mocha" +Set FontSize 14 +Set Width 1200 +Set Height 720 +Set Padding 20 +Set TypingSpeed 50ms +Set Framerate 24 +Set PlaybackSpeed 1.0 + +Hide +Type "clear" +Enter +Type "sudo -A target/release/rustnet" +Enter +Sleep 2500ms +Show + +Sleep 3s + +Down +Sleep 1s +Down +Sleep 1s +Down +Sleep 1500ms + +Type "/" +Sleep 400ms +Type "443" +Sleep 1500ms +Escape +Sleep 800ms + +Type "a" +Sleep 1500ms +Down +Sleep 600ms +Space +Sleep 1500ms +Type "a" +Sleep 800ms + +Enter +Sleep 3s + +Escape +Sleep 600ms + +Tab@400ms 3 +Sleep 3s + +Escape +Sleep 1500ms + +Hide +Type "q" +Sleep 200ms +Type "q" diff --git a/screenshots.tape b/screenshots.tape new file mode 100644 index 0000000..e1042a3 --- /dev/null +++ b/screenshots.tape @@ -0,0 +1,96 @@ +# vhs tape for the rustnet README screenshots. +# +# Render with: scripts/record-rustnet-demo.sh +# (which runs both demo.tape and this tape, then cleans the throwaway GIF). +# +# Manual render: vhs screenshots.tape +# Pre-build the release binary first: cargo build --release +# +# Walks every tab and emits PNGs under assets/screenshots/. +# Sized larger than demo.tape (1600x1000, FontSize 16) for crisper README +# rendering. The GIF Output is a throwaway and is deleted by the script. +# +# Beats and outputs: +# 1. Overview -> assets/screenshots/overview.png +# 2. Filter mode (/port:443) -> assets/screenshots/filter.png +# 3. Process grouping (a, Space) -> assets/screenshots/grouping.png +# 4. Details (Enter) -> assets/screenshots/details.png +# 5. Graph (Tab x3) -> assets/screenshots/graph.png +# 6. Interfaces (i) -> assets/screenshots/interfaces.png + +Output assets/screenshots/_throwaway.gif + +Set Theme "Catppuccin Mocha" +Set FontSize 16 +Set Width 1600 +Set Height 1000 +Set Padding 20 +Set TypingSpeed 50ms +Set Framerate 24 +Set PlaybackSpeed 1.0 + +Hide +Type "clear" +Enter +Type "sudo -A target/release/rustnet" +Enter +Sleep 2500ms +Show + +# 1. Overview +Sleep 3s +Down +Sleep 500ms +Down +Sleep 500ms +Screenshot "assets/screenshots/overview.png" +Sleep 500ms + +# 2. Filter mode +Type "/" +Sleep 400ms +Type "port:443" +Sleep 1500ms +Screenshot "assets/screenshots/filter.png" +Sleep 500ms +Escape +Sleep 800ms + +# 3. Process grouping +Type "a" +Sleep 1500ms +Down +Sleep 400ms +Space +Sleep 1500ms +Screenshot "assets/screenshots/grouping.png" +Sleep 500ms +Type "a" +Sleep 800ms + +# 4. Details +Enter +Sleep 2500ms +Screenshot "assets/screenshots/details.png" +Sleep 500ms +Escape +Sleep 800ms + +# 5. Graph +Tab@400ms 3 +Sleep 2500ms +Screenshot "assets/screenshots/graph.png" +Sleep 500ms +Escape +Sleep 800ms + +# 6. Interfaces +Type "i" +Sleep 2500ms +Screenshot "assets/screenshots/interfaces.png" +Sleep 500ms + +Hide +Type "q" +Sleep 200ms +Type "q" diff --git a/scripts/record-rustnet-demo.sh b/scripts/record-rustnet-demo.sh new file mode 100755 index 0000000..a7e3c73 --- /dev/null +++ b/scripts/record-rustnet-demo.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Automate the rustnet VHS recording: produces assets/rustnet.gif (showcase) +# and assets/screenshots/*.png (README) in one pass. +# +# Prerequisites: +# brew install vhs # required +# brew install gifsicle # optional, for GIF size optimization +# cargo build --release # auto-run if target/release/rustnet missing +# +# Usage: +# scripts/record-rustnet-demo.sh +# +# What it does: +# 1. Verifies vhs and cargo are installed. +# 2. Builds target/release/rustnet if missing or stale vs Cargo.lock. +# 3. Pre-caches sudo (PKTAP capture on macOS always needs root). +# 4. Spawns a sudo keepalive loop so a long render does not lose creds. +# 5. Spawns a background traffic generator (curl/dig/ping) so the +# connection table is busy and sparklines move. +# 6. Runs `vhs demo.tape` -> assets/rustnet.gif. +# 7. Runs `vhs screenshots.tape` -> assets/screenshots/*.png. +# 8. Optimizes the GIF with gifsicle -O3 if available. +# 9. Cleans up the throwaway GIF and background processes. +# 10. Prints a summary of the produced assets. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +RUSTNET_DIR="$(dirname "$SCRIPT_DIR")" +DEMO_TAPE="${RUSTNET_DIR}/demo.tape" +SCREENSHOTS_TAPE="${RUSTNET_DIR}/screenshots.tape" +GIF_FILE="${RUSTNET_DIR}/assets/rustnet.gif" +SCREENSHOTS_DIR="${RUSTNET_DIR}/assets/screenshots" +THROWAWAY_GIF="${SCREENSHOTS_DIR}/_throwaway.gif" +RELEASE_BIN="${RUSTNET_DIR}/target/release/rustnet" + +TRAFFIC_PID="" +ASKPASS_SCRIPT="" +PW_FILE="" + +cleanup() { + if [[ -n "${TRAFFIC_PID}" ]] && kill -0 "${TRAFFIC_PID}" 2>/dev/null; then + kill "${TRAFFIC_PID}" 2>/dev/null || true + wait "${TRAFFIC_PID}" 2>/dev/null || true + fi + [[ -n "${ASKPASS_SCRIPT}" ]] && rm -f "${ASKPASS_SCRIPT}" + [[ -n "${PW_FILE}" ]] && rm -f "${PW_FILE}" + rm -f "${THROWAWAY_GIF}" +} +trap cleanup EXIT INT TERM + +# Kill stale ttyd / headless-Chrome processes left over from prior failed +# vhs runs. VHS uses go-rod, which writes its Chrome profile to a +# `rod/user-data` tmpdir; if a prior run died, the locked profile causes +# the next run to fail with `could not open ttyd: ERR_CONNECTION_REFUSED`. +preflight_cleanup_vhs() { + local stale=0 + if pgrep -f "ttyd --port" >/dev/null 2>&1; then + pkill -f "ttyd --port" 2>/dev/null || true + stale=1 + fi + if pgrep -f "rod/user-data" >/dev/null 2>&1; then + pkill -f "rod/user-data" 2>/dev/null || true + stale=1 + fi + if [[ "${stale}" -eq 1 ]]; then + echo "Cleaned up stale ttyd / Chrome processes from prior vhs run." + sleep 1 + fi + rm -rf "${TMPDIR:-/tmp}/rod" 2>/dev/null || true +} +preflight_cleanup_vhs + +# Dependency checks +for cmd in vhs cargo; do + if ! command -v "$cmd" &> /dev/null; then + echo "Error: $cmd is not installed." + [[ "$cmd" == "vhs" ]] && echo "Install: brew install vhs" + [[ "$cmd" == "cargo" ]] && echo "Install: https://rustup.rs" + exit 1 + fi +done + +if ! command -v gifsicle &> /dev/null; then + echo "Note: gifsicle not found; skipping GIF optimization." + echo " Install with: brew install gifsicle" + HAS_GIFSICLE=0 +else + HAS_GIFSICLE=1 +fi + +# Tape files must exist +[[ -f "${DEMO_TAPE}" ]] || { echo "Error: ${DEMO_TAPE} missing."; exit 1; } +[[ -f "${SCREENSHOTS_TAPE}" ]] || { echo "Error: ${SCREENSHOTS_TAPE} missing."; exit 1; } + +# Build release binary if missing or stale +if [[ ! -x "${RELEASE_BIN}" ]] || [[ "${RUSTNET_DIR}/Cargo.lock" -nt "${RELEASE_BIN}" ]]; then + echo "Building release binary..." + (cd "${RUSTNET_DIR}" && cargo build --release) +fi + +# Read sudo password and validate it. macOS sudo uses tty_tickets by default, +# so a `sudo -v` cached in this shell does NOT carry over to the new pty +# that vhs/ttyd spawns. Instead, store the validated password in a private +# file and expose it via SUDO_ASKPASS so the in-tape `sudo -A target/release/rustnet` +# can re-authenticate non-interactively. +echo "Capture requires root. Enter your sudo password (3 attempts)." +SUDO_PW="" +for attempt in 1 2 3; do + printf 'Password: ' + IFS= read -rs SUDO_PW + printf '\n' + if [[ -z "${SUDO_PW}" ]]; then + echo " empty password, try again ($attempt/3)" + continue + fi + if printf '%s\n' "${SUDO_PW}" | sudo -S -v 2>/dev/null; then + break + fi + echo " wrong password ($attempt/3)" + SUDO_PW="" +done +if [[ -z "${SUDO_PW}" ]]; then + echo "Error: sudo authentication failed after 3 attempts." + exit 1 +fi + +# Stash the password in a 0600 temp file and write a tiny askpass helper +# that cats it. SUDO_ASKPASS in the env makes `sudo -A` use this helper. +PW_FILE="$(mktemp -t rustnet-pw.XXXXXX)" +chmod 600 "${PW_FILE}" +printf '%s' "${SUDO_PW}" > "${PW_FILE}" +unset SUDO_PW + +ASKPASS_SCRIPT="$(mktemp -t rustnet-askpass.XXXXXX)" +chmod 700 "${ASKPASS_SCRIPT}" +cat > "${ASKPASS_SCRIPT}" </dev/null 2>&1 || true + dig +short +time=2 +tries=1 github.com >/dev/null 2>&1 || true + ping -c 1 -W 1 1.1.1.1 >/dev/null 2>&1 || true + sleep 1 + done +) & +TRAFFIC_PID=$! + +# Render the demo GIF. +# VHS does not propagate the parent shell's env to the recording shell. +# Inject an `Env SUDO_ASKPASS ` directive into each tape so the in-tape +# `sudo -A` can find the askpass helper. VHS requires Env to come AFTER all +# `Set` directives but BEFORE the first command (Hide/Type/Sleep/etc.), so +# we use awk to insert it at the first command line. +render_tape_with_askpass() { + local tape="$1" + local tmp_tape + tmp_tape="$(mktemp -t "$(basename "${tape}").XXXXXX")" + awk -v askpass="${ASKPASS_SCRIPT}" ' + BEGIN { injected = 0 } + /^(Output|Set|Env|Require|Source|#|[[:space:]]*$)/ { print; next } + { + if (!injected) { + print "Env SUDO_ASKPASS \"" askpass "\"" + injected = 1 + } + print + } + ' "${RUSTNET_DIR}/${tape}" > "${tmp_tape}" + (cd "${RUSTNET_DIR}" && vhs "${tmp_tape}") + rm -f "${tmp_tape}" +} + +echo "" +echo "Rendering demo GIF (vhs demo.tape)..." +render_tape_with_askpass demo.tape + +# Render the screenshots. +echo "" +echo "Rendering screenshots (vhs screenshots.tape)..." +render_tape_with_askpass screenshots.tape + +# Optimize the GIF. +if [[ "${HAS_GIFSICLE}" -eq 1 ]] && [[ -f "${GIF_FILE}" ]]; then + echo "" + echo "Optimizing GIF with gifsicle -O3..." + BEFORE_SIZE=$(stat -f%z "${GIF_FILE}" 2>/dev/null || stat -c%s "${GIF_FILE}") + gifsicle -O3 --batch "${GIF_FILE}" + AFTER_SIZE=$(stat -f%z "${GIF_FILE}" 2>/dev/null || stat -c%s "${GIF_FILE}") + echo " ${BEFORE_SIZE} -> ${AFTER_SIZE} bytes" +fi + +# Summary. +echo "" +echo "Done." +echo "" +echo "GIF:" +ls -lh "${GIF_FILE}" 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' +echo "" +echo "Screenshots:" +for png in "${SCREENSHOTS_DIR}"/*.png; do + [[ -f "${png}" ]] && ls -lh "${png}" | awk '{print " " $9 " (" $5 ")"}' +done +echo "" +echo "Preview:" +echo " open ${GIF_FILE}" +echo " open ${SCREENSHOTS_DIR}"