#!/bin/sh # nnn-probe.sh 1.0 - the No Noisy Neighbors probe. https://nonoisyneighbors.com/tools/probe # # Measures how much a server is disturbed by its neighbours, over time: # - CPU steal and I/O wait, from /proc/stat, once per interval # - "missing" CPU time: ticks the kernel never accounted for at all (some hosts hide steal # that way), from /proc/stat against /proc/uptime # - disk write latency: small synced writes with dd (or ioping, if installed); no fio # - network round trip, jitter and loss: a few pings to one target per interval # Then it prints a summary and, unless you pass -n, sends the summary to No Noisy Neighbors, # signed with your API key (HMAC-SHA256), where it becomes a [Steal Report] post. # # Read it before you run it. It needs no root, installs nothing, writes only a temporary # directory (removed at the end) and sends only the numbers in the summary plus what you # pass with -p, -P and -l. Your IP address is seen by the server, as with any web request. # # Usage: # sh nnn-probe.sh -m 10m|24h|7d -p PROVIDER -P "PLAN" -l "LOCATION" [-t PING_TARGET] [-k KEY] [-n] # -m 10m: 120 samples, every 5 s 24h: 1440 samples, every 60 s 7d: 2016 samples, every 300 s # -p the provider's slug on nonoisyneighbors.com (the part after /providers/) # -P plan name, -l location (city or data centre), as you would tell a friend # -t ping target (default 1.1.1.1) # -k API key "keyid.secret" (or set NNN_KEY); create one at /tools/probe # -n measure and print only; send nothing # For the long modes, keep it running with: nohup sh nnn-probe.sh ... > probe.log 2>&1 & # # Licence: MIT. Copyright (c) 2026 OptiTide. set -u VERSION=1.0 API=${NNN_API:-https://nonoisyneighbors.com/api/telemetry} MODE=10m PROVIDER="" PLAN="" LOCATION="" TARGET=1.1.1.1 KEY=${NNN_KEY:-} SEND=1 LC_ALL=C export LC_ALL die() { echo "nnn-probe: $*" >&2; exit 1; } clean() { printf '%s' "$1" | tr -d '\r\n=' | cut -c1-80; } while getopts "m:p:P:l:t:k:nh" opt; do case $opt in m) MODE=$OPTARG ;; p) PROVIDER=$OPTARG ;; P) PLAN=$OPTARG ;; l) LOCATION=$OPTARG ;; t) TARGET=$OPTARG ;; k) KEY=$OPTARG ;; n) SEND=0 ;; *) sed -n '2,30p' "$0"; exit 0 ;; esac done case $MODE in 10m) INTERVAL=5; SAMPLES=120 ;; 24h) INTERVAL=60; SAMPLES=1440 ;; 7d) INTERVAL=300; SAMPLES=2016 ;; *) die "mode must be 10m, 24h or 7d" ;; esac [ -r /proc/stat ] && [ -r /proc/uptime ] || die "this needs Linux (/proc/stat and /proc/uptime)" case $TARGET in *[!A-Za-z0-9.:-]*|'') die "the ping target must be a host name or IP address" ;; esac if [ "$SEND" = 1 ]; then [ -n "$PROVIDER" ] && [ -n "$PLAN" ] && [ -n "$LOCATION" ] || die "to send a report, give -p PROVIDER -P PLAN -l LOCATION (or use -n)" case $PROVIDER in *[!a-z0-9-]*) die "-p takes the provider slug, for example example-hosting" ;; esac case $KEY in ?*.?*) ;; *) die "to send a report you need an API key (-k or NNN_KEY); create one at https://nonoisyneighbors.com/tools/probe" ;; esac command -v openssl >/dev/null 2>&1 || die "openssl is needed to sign the report (or use -n)" command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 || die "curl or wget is needed to send the report (or use -n)" fi WORK=$(mktemp -d "${TMPDIR:-/tmp}/nnn-probe.XXXXXX" 2>/dev/null) || { WORK="${TMPDIR:-/tmp}/nnn-probe.$$"; mkdir -m 700 "$WORK" || die "cannot create a work directory"; } trap 'rm -rf "$WORK"' EXIT trap 'exit 130' INT TERM : > "$WORK/cpu"; : > "$WORK/disk"; : > "$WORK/ping" HZ=$(getconf CLK_TCK 2>/dev/null || echo 100) NCPU=$(grep -c '^processor' /proc/cpuinfo 2>/dev/null || echo 1) [ "$NCPU" -ge 1 ] 2>/dev/null || NCPU=1 # total-ticks iowait steal uptime-centiseconds snap() { awk '/^cpu / { t = 0; for (i = 2; i <= 9; i++) t += $i; printf "%s %s %s", t, $6, $9; exit }' /proc/stat awk '{ printf " %d\n", $1 * 100 }' /proc/uptime } DISK_METHOD=none if command -v ioping >/dev/null 2>&1 && ioping -c 1 -q -D "$WORK" >/dev/null 2>&1; then DISK_METHOD=ioping elif dd if=/dev/zero of="$WORK/dd.tmp" bs=4096 count=1 oflag=dsync 2>&1 | grep -q ' s,'; then DISK_METHOD=dd fi disk_once() { case $DISK_METHOD in ioping) ioping -c 3 -q -B -D "$WORK" 2>/dev/null | awk '{ if ($6 > 0) printf "%.3f\n", $6 / 1000000 }' >> "$WORK/disk" ;; dd) dd if=/dev/zero of="$WORK/dd.tmp" bs=4096 count=4 oflag=dsync 2>&1 \ | awk '/ s,/ { for (i = 1; i <= NF; i++) if ($i == "s,") { printf "%.3f\n", $(i - 1) * 1000 / 4; exit } }' >> "$WORK/disk" ;; esac } PING_OK=0 if command -v ping >/dev/null 2>&1 && ping -c 1 -W 2 "$TARGET" >/dev/null 2>&1; then PING_OK=1; fi ping_once() { [ "$PING_OK" = 1 ] || return 0 ping -c 5 -i 0.2 -W 2 -q "$TARGET" 2>/dev/null | awk ' /packet loss/ { for (i = 1; i <= NF; i++) if ($i ~ /%$/) { sub(/%/, "", $i); loss = $i } } /min\/avg\/max/ { split($0, a, "= "); split(a[2], v, "/"); sub(/ .*/, "", v[4]); avg = v[2]; jit = (v[4] != "" ? v[4] : (v[3] - v[1]) / 2) } END { if (loss != "") printf "%s %s %s\n", (avg == "" ? "-" : avg), (jit == "" ? "-" : jit), loss }' >> "$WORK/ping" } STARTED=$(date +%s) echo "nnn-probe $VERSION: $MODE run, $SAMPLES samples every ${INTERVAL}s, disk test: $DISK_METHOD, ping: $([ "$PING_OK" = 1 ] && echo "$TARGET" || echo off)" i=0 prev=$(snap) while [ "$i" -lt "$SAMPLES" ]; do i=$((i + 1)) disk_once ping_once rest=$((STARTED + i * INTERVAL - $(date +%s))); [ "$rest" -lt 1 ] && rest=1 sleep "$rest" cur=$(snap) echo "$prev $cur" | awk -v hz="$HZ" -v n="$NCPU" '{ dt = $5 - $1; if (dt <= 0) exit steal = 100 * ($7 - $3) / dt; io = 100 * ($6 - $2) / dt wall = ($8 - $4) / 100 * hz * n; miss = (wall > 0) ? 100 * (1 - dt / wall) : 0; if (miss < 0) miss = 0 printf "%.3f %.3f %.3f\n", steal, io, miss }' >> "$WORK/cpu" prev=$cur if [ $((i % 12)) -eq 0 ]; then echo " $i/$SAMPLES samples"; fi done ENDED=$(date +%s) # mean p50 p95 max of column $2 in file $1 stats() { awk -v c="$2" '$c != "-" && $c != "" { print $c }' "$1" | sort -n | awk ' { v[NR] = $1; s += $1 } END { if (NR == 0) { print "- - - -"; exit } i50 = int(0.50 * (NR - 1)) + 1; i95 = int(0.95 * (NR - 1)) + 1 printf "%.3f %.3f %.3f %.3f\n", s / NR, v[i50], v[i95], v[NR] }' } set -- $(stats "$WORK/cpu" 1); ST_MEAN=$1 ST_P50=$2 ST_P95=$3 ST_MAX=$4 set -- $(stats "$WORK/cpu" 2); IO_MEAN=$1 IO_P50=$2 IO_P95=$3 set -- $(stats "$WORK/cpu" 3); MS_P50=$2 MS_P95=$3 set -- $(stats "$WORK/disk" 1); DK_P50=$2 DK_P95=$3 set -- $(stats "$WORK/ping" 1); RTT_P50=$2 set -- $(stats "$WORK/ping" 2); JIT_P50=$2 JIT_P95=$3 LOSS=$(awk '{ s += $3 } END { if (NR) printf "%.2f", s / NR; else print "-" }' "$WORK/ping") # Steal over time, averaged into at most 288 points. SERIES=$(awk '{ print $1 }' "$WORK/cpu" | awk -v max=288 '{ v[NR] = $1 } END { b = (NR > max) ? NR / max : 1; out = "" for (k = 0; k * b < NR; k++) { s = 0; c = 0; for (j = int(k * b) + 1; j <= int((k + 1) * b) && j <= NR; j++) { s += v[j]; c++ } if (c) out = out (out == "" ? "" : ",") sprintf("%.2f", s / c) } print out }') VIRT=$( (systemd-detect-virt 2>/dev/null || echo unknown) | head -1 | tr -cd 'a-z0-9-') KERNEL=$(uname -r | tr -cd 'A-Za-z0-9._+-' | cut -c1-80) cat < "$WORK/report" if [ "$SEND" = 0 ]; then echo echo "Not sent (-n). This is what would be sent:" cat "$WORK/report" exit 0 fi KEYID=${KEY%%.*} SECRET=${KEY#*.} TS=$(date +%s) SIG=$( { printf '%s\n' "$TS"; cat "$WORK/report"; } | openssl dgst -sha256 -hmac "$SECRET" | awk '{ print $NF }') UA="NoNoisyNeighbors-Probe/$VERSION" if command -v curl >/dev/null 2>&1; then OUT=$(curl -sS -m 60 -A "$UA" -H "Content-Type: text/plain" -H "X-NNN-Key: $KEYID" -H "X-NNN-Timestamp: $TS" \ -H "X-NNN-Signature: $SIG" --data-binary @"$WORK/report" -w '\n%{http_code}' "$API") || die "could not reach $API" else OUT=$(wget -q -O - -T 60 -U "$UA" --header="Content-Type: text/plain" --header="X-NNN-Key: $KEYID" --header="X-NNN-Timestamp: $TS" \ --header="X-NNN-Signature: $SIG" --post-file="$WORK/report" "$API" && { echo; echo 200; }) || die "the server refused the report (wget hides the reason; try curl)" fi CODE=$(printf '%s\n' "$OUT" | tail -n 1) BODY=$(printf '%s\n' "$OUT" | sed '$d') if [ "$CODE" = 200 ] || [ "$CODE" = 201 ]; then echo echo "Sent. $(printf '%s' "$BODY" | sed -n 's/.*"url":"\([^"]*\)".*/Your report: \1/p')" else die "the server answered $CODE: $BODY" fi