#!/bin/bash set -uo pipefail trap 'echo "[✖] Execution halted at line $LINENO. Please consult your nearest bash therapist." >&2' ERR trap 'echo "[✌️] Script exited cleanly. Have a burrito." >&2' EXIT NO_ENRICH=0 NO_IPERF=0 LISTENER_ENABLED=0 LISTENER_ARGS=() while [[ $# -gt 0 ]]; do case "$1" in --no-enrich) NO_ENRICH=1 shift ;; --listener) LISTENER_ENABLED=1 NO_ENRICH=1 shift ;; --listener-channel=*) LISTENER_ARGS+=("--channel" "${1#*=}") shift ;; --listener-hop) LISTENER_ARGS+=("--channel-hop") shift ;; --no-iperf) NO_IPERF=1 shift ;; *) echo "Unknown argument: $1" exit 1 ;; esac done IFS=$'\n\t' echo "Here are the arguments passed to the script: $@" # === Now start your real script === source settings.env # (redefine BOOT_ID, TEST_FILE, etc after reloading settings.env if needed) BOOT_ID=$(cat /proc/sys/kernel/random/boot_id) TEST_FILE="${HOME}/${BOOT_ID}-speedtest.csv" if [ "$LISTENER_ENABLED" -eq 1 ]; then LISTENER_OUTFILE="${TEST_FILE}" else LISTENER_OUTFILE="" fi ENRICHED_FILE="${TEST_FILE%.csv}+rf.csv" SSID_METRICS_FILE="${ENRICHED_FILE%.csv}-ssid-metrics.csv" FAILURE_LOG="${TEST_FILE%.csv}-failures.log" SCRIPT_START=$(date +%s) EMAIL_BODY=${EMAIL_BODY:-"Test $BOOT_ID completed successfully. Please find the attached files and collect the probe."} log() { echo "[+] $*" >&2 } warn() { echo "[!] $*" >&2 } die() { warn "$*" exit 1 } [ -z "${RECIPIENT:-}" ] && die "Please set the RECIPIENT variable in settings.env." log "Test file: $TEST_FILE" log "Enriched file: $ENRICHED_FILE" log "SSID metrics file: $SSID_METRICS_FILE" sudo -v while true; do sudo -n true sleep 60 done 2>/dev/null & SUDO_KEEPALIVE_PID=$! if [ "$LISTENER_ENABLED" -eq 0 ]; then log "Starting kismet..." sudo systemctl start kismet log "Saturating the capture..." sleep "$LEAD_TIME" fi get_tx_failed() { iw dev "$INTERFACE" station dump | awk '/tx failed/ {print $3}' } freq_to_channel() { local freq=$1 if ((freq >= 2412 && freq <= 2472)); then echo $(((freq - 2407) / 5)) elif ((freq == 2484)); then echo 14 elif ((freq >= 5180 && freq <= 5825)); then echo $(((freq - 5000) / 5)) else echo "Unknown"; fi } run_iperf() { local target=$1 mode=$2 direction=$3 local args=("-c" "$target" "-J" "-t" "$IPERF_DURATION") [ "$mode" = "udp" ] && args+=("-u" "-b" "$BANDWIDTH") [ "$direction" = "down" ] && args+=("--reverse") local tmp_err=$(mktemp) tmp_json=$(mktemp) local timeout_duration=$((IPERF_DURATION + 20)) log "Running iperf3 $mode $direction to $target..." if ! timeout "${timeout_duration}s" iperf3 "${args[@]}" >"$tmp_json" 2>"$tmp_err"; then warn "iperf3 $mode $direction to $target failed or timed out" echo "0" return fi local parsed parsed=$(jq -r ' if .error then "iperf3-error" elif has("end") | not then "no-end" elif .end | has("sum_received") then .end.sum_received.bits_per_second elif .end | has("sum") then .end.sum.bits_per_second else "unexpected-format" end' "$tmp_json" 2>/dev/null || echo "execution-failed") if [[ "$parsed" =~ ^(iperf3-error|no-end|unexpected-format|execution-failed)$ ]]; then timestamp=$(date -Iseconds) echo "$timestamp,iperf $mode $direction to $target failed with '$parsed'" >>"$FAILURE_LOG" echo "[stderr] $(cat "$tmp_err")" >>"$FAILURE_LOG" echo "[json] $(cat "$tmp_json")" >>"$FAILURE_LOG" echo "0" else echo "$parsed" fi rm -f "$tmp_err" "$tmp_json" } # Send start email echo -e "Subject: Test ${BOOT_ID} Started\n\nTest ${BOOT_ID} has commenced." | msmtp "$RECIPIENT" FAILED_START=$(get_tx_failed) DUMMY_SPEEDTEST=$(speedtest --format=csv --output-header) SPEEDTEST_HEADER=$(echo "$DUMMY_SPEEDTEST" | head -n 1) # CSV setup TEST_HEADER="StartTimestamp,EndTimestamp,Link,Level,Noise,BSSID,TX Bitrate,RX Bitrate,${SPEEDTEST_HEADER},TX Failures,Channel,Frequency,Packet Loss,Jitter,LocalTCPUp,LocalTCPDown,LocalUDPUp,LocalUDPDown,RemoteTCPUp,RemoteTCPDown,RemoteUDPUp,RemoteUDPDown" LISTENER_HEADER="Timestamp,ClientsOnAP,ClientsOnChannel,APsOnChannel,AvgAPSignal,StrongestAPSignal,UnlinkedDevices,NumberofBSSIDsOnSSID,AvgSSIDSignal,MaxSSIDSignal,NumberofChannelsOnSSID,PacketCount,Deadpoints" echo "Speedtest,LocalTCPUp,LocalTCPDown,LocalUDPUp,LocalUDPDown,RemoteTCPUp,RemoteTCPDown,RemoteUDPUp,RemoteUDPDown" >"${TEST_FILE}_durations.csv" if [ ! -f "$TEST_FILE" ]; then if [ "$LISTENER_ENABLED" -eq 1 ]; then echo "$TEST_HEADER,$LISTENER_HEADER" >"$TEST_FILE" else echo "$TEST_HEADER" >"$TEST_FILE" fi fi ESTIMATED_OBS_DURATION=60 for ((COUNTER = 1; COUNTER <= NUM_TESTS; COUNTER++)); do log "Test run $COUNTER of $NUM_TESTS" for ((i = 1; i <= NUM_SAMPLES; i++)); do if [ "$LISTENER_ENABLED" -eq 1 ]; then LISTENER_SAMPLE_FILE="${TEST_FILE%.csv}-listener-$COUNTER-$i.csv" log "Launching listener for sample $i/$NUM_TESTS..." READY_FILE="/tmp/listener_ready_${COUNTER}_${i}" ADAPTIVE_ARGS=("${LISTENER_ARGS[@]}") if [[ " ${LISTENER_ARGS[*]} " =~ "--channel-hop" ]]; then NUM_CHANNELS=15 # Update if your list changes HOP_INTERVAL=$((ESTIMATED_OBS_DURATION / (NUM_CHANNELS + 1))) [[ "$HOP_INTERVAL" -lt 2 ]] && HOP_INTERVAL=2 # Don't go too fast log "Estimated observation time: ${ESTIMATED_OBS_DURATION}s → Setting hop interval: ${HOP_INTERVAL}s" ADAPTIVE_ARGS+=("--hop-interval" "$HOP_INTERVAL") fi log "Launching listener with args: --main-iface $INTERFACE --monitor-iface $LISTEN_INTERFACE --outfile $LISTENER_SAMPLE_FILE ${ADAPTIVE_ARGS[*]}" sudo "${SCRIPT_DIRECTORY}/listener.py" \ --main-iface "$INTERFACE" \ --monitor-iface "$LISTEN_INTERFACE" \ --outfile "$LISTENER_SAMPLE_FILE" \ "${ADAPTIVE_ARGS[@]}" > >(tee "$READY_FILE") & LISTENER_PID=$! # Wait for the READY_FILE to contain "LISTENING_STARTED" bash -c "until grep -q LISTENING_STARTED $READY_FILE; do sleep 0.2; done" fi log " Sample $i of $NUM_SAMPLES" START_TIME=$(date -Iseconds) link_level_noise=$(awk 'NR==3 {gsub(/\./, "", $3); gsub(/\./, "", $4); gsub(/\./, "", $5); print $3","$4","$5}' /proc/net/wireless) bssid_and_bitrate=$(iw dev "$INTERFACE" link | awk '/Connected/ {bssid=$3} /tx bitrate/ {tx=$3} /rx bitrate/ {rx=$3} END {print bssid","tx","rx}') speed_results="" speedtest_duration=0 localtcpup_duration=0 localtcpdown_duration=0 localudpup_duration=0 localudpdown_duration=0 remotetcpup_duration=0 remotetcpdown_duration=0 remoteudpup_duration=0 remoteudpdown_duration=0 for ((retry = 1; retry <= MAX_RETRIES; retry++)); do SECONDS=0 log "Speed test attempt $retry" # speed_results=$(speedtest --secure --csv --server 57444 --server 16797 --server 40818 --server 24079 --server 55661 2>/dev/null || true) speed_results=$(speedtest --format=csv 2>/dev/null || true) speedtest_duration=$SECONDS [[ -n "$speed_results" ]] && break warn "Speedtest failed. Retrying in $RETRY_DELAY seconds..." echo "$(date -Iseconds),Speedtest failed on attempt $retry for test $COUNTER, sample $i" >>"$FAILURE_LOG" sleep "$RETRY_DELAY" done if [[ -z "$speed_results" ]]; then timestamp=$(date -Iseconds) warn "Speedtest permanently failed. Skipping sample." echo "$timestamp,Test $COUNTER,Sample $i" >>"$FAILURE_LOG" speedtest_duration=$SECONDS continue fi FAILED_NOW=$(get_tx_failed) FAILED_DELTA=$((FAILED_NOW - FAILED_START)) FAILED_START=$FAILED_NOW freq=$(iw dev "$INTERFACE" link | awk '/freq:/ {print $2}') channel=$(freq_to_channel "$freq") log " Running ping test..." ping_output=$(ping -c "$PING_COUNT" "$PING_TARGET") packet_loss=$(echo "$ping_output" | grep -oP '\d+(?=% packet loss)') jitter=$(echo "$ping_output" | grep "time=" | awk '{print $(NF-1)}' | sed 's/time=//g' | awk '{sum+=$1; sumsq+=$1*$1} END {if (NR>1) print sqrt(sumsq/NR - (sum/NR)**2); else print 0}') if [ "$NO_IPERF" -eq 0 ]; then log " Running iperf3 tests..." SECONDS=0 LocalTCPDown=$(run_iperf "$IPERF_LOCAL_TARGET" tcp down) localtcpdown_duration=$SECONDS SECONDS=0 LocalUDPUp=$(run_iperf "$IPERF_LOCAL_TARGET" udp up) localudpup_duration=$SECONDS SECONDS=0 LocalUDPDown=$(run_iperf "$IPERF_LOCAL_TARGET" udp down) localudpdown_duration=$SECONDS SECONDS=0 RemoteTCPUp=$(run_iperf "$IPERF_REMOTE_TARGET" tcp up) remotetcpup_duration=$SECONDS SECONDS=0 RemoteTCPDown=$(run_iperf "$IPERF_REMOTE_TARGET" tcp down) remotetcpdown_duration=$SECONDS SECONDS=0 RemoteUDPUp=$(run_iperf "$IPERF_REMOTE_TARGET" udp up) remoteudpup_duration=$SECONDS SECONDS=0 RemoteUDPDown=$(run_iperf "$IPERF_REMOTE_TARGET" udp down) remoteudpdown_duration=$SECONDS else log " Skipping iperf3 tests as per --no-iperf flag" LocalTCPUp=0 LocalTCPDown=0 LocalUDPUp=0 LocalUDPDown=0 RemoteTCPUp=0 RemoteTCPDown=0 RemoteUDPUp=0 RemoteUDPDown=0 localtcpup_duration=0 localtcpdown_duration=0 localudpup_duration=0 localudpdown_duration=0 remotetcpup_duration=0 remotetcpdown_duration=0 remoteudpup_duration=0 remoteudpdown_duration=0 fi if [ "$LISTENER_ENABLED" -eq 1 ]; then if [ -n "${LISTENER_PID:-}" ]; then log "Stopping listener (PID $LISTENER_PID)..." sudo kill -SIGINT "$LISTENER_PID" wait "$LISTENER_PID" fi fi END_TIME=$(date -Iseconds) TOTAL_DURATION=$((speedtest_duration + localtcpup_duration + localtcpdown_duration + localudpup_duration + localudpdown_duration + remotetcpup_duration + remotetcpdown_duration + remoteudpup_duration + remoteudpdown_duration + PING_COUNT)) [[ "$TOTAL_DURATION" -lt 10 ]] && TOTAL_DURATION=60 # fallback sanity value ESTIMATED_OBS_DURATION=$TOTAL_DURATION if [ "$LISTENER_ENABLED" -eq 1 ] && [ -n "${LISTENER_SAMPLE_FILE:-}" ]; then if [ -f "$LISTENER_SAMPLE_FILE" ]; then LISTENER_ROW=$(tail -n 1 "$LISTENER_SAMPLE_FILE") if [ -n "$LISTENER_ROW" ]; then echo "$START_TIME,$END_TIME,$link_level_noise,$bssid_and_bitrate,$speed_results,$FAILED_DELTA,$channel,$freq,$packet_loss,$jitter,$LocalTCPUp,$LocalTCPDown,$LocalUDPUp,$LocalUDPDown,$RemoteTCPUp,$RemoteTCPDown,$RemoteUDPUp,$RemoteUDPDown,$LISTENER_ROW" >>"$TEST_FILE" else warn "Listener output file $LISTENER_SAMPLE_FILE was empty. Appending blank listener columns." echo "$START_TIME,$END_TIME,$link_level_noise,$bssid_and_bitrate,$speed_results,$FAILED_DELTA,$channel,$freq,$packet_loss,$jitter,$LocalTCPUp,$LocalTCPDown,$LocalUDPUp,$LocalUDPDown,$RemoteTCPUp,$RemoteTCPDown,$RemoteUDPUp,$RemoteUDPDown,$(yes ',' | head -n 12 | tr -d '\n')" >>"$TEST_FILE" fi rm -f "$LISTENER_SAMPLE_FILE" else warn "Listener sample file $LISTENER_SAMPLE_FILE not found. Appending blank listener columns." echo "$START_TIME,$END_TIME,$link_level_noise,$bssid_and_bitrate,$speed_results,$FAILED_DELTA,$channel,$freq,$packet_loss,$jitter,$LocalTCPUp,$LocalTCPDown,$LocalUDPUp,$LocalUDPDown,$RemoteTCPUp,$RemoteTCPDown,$RemoteUDPUp,$RemoteUDPDown,$(yes ',' | head -n 12 | tr -d '\n')" >>"$TEST_FILE" fi else echo "$START_TIME,$END_TIME,$link_level_noise,$bssid_and_bitrate,$speed_results,$FAILED_DELTA,$channel,$freq,$packet_loss,$jitter,$LocalTCPUp,$LocalTCPDown,$LocalUDPUp,$LocalUDPDown,$RemoteTCPUp,$RemoteTCPDown,$RemoteUDPUp,$RemoteUDPDown" >>"$TEST_FILE" fi echo "$START_TIME,$speedtest_duration,$localtcpup_duration,$localtcpdown_duration,$localudpup_duration,$localudpdown_duration,$remotetcpup_duration,$remotetcpdown_duration,$remoteudpup_duration,$remoteudpdown_duration" >>"${TEST_FILE}_durations.csv" done [[ "$COUNTER" -lt "$NUM_TESTS" ]] && log "Waiting $TIME_BETWEEN before next test..." && sleep "$TIME_BETWEEN" done if [ "$LISTENER_ENABLED" -eq 0 ]; then log "Stopping kismet..." sudo systemctl stop kismet fi if [ "$NO_ENRICH" -eq 0 ]; then log "Enriching data..." KISMET_LOG=$(find "$KISMET_LOG_DIR" -type f -name "*.pcapng" -printf "%T@ %p\n" | sort -n | tail -1 | cut -d' ' -f2-) [ ! -f "$KISMET_LOG" ] && die "Packet capture not found." SECONDS=0 python3 "$SCRIPT_DIRECTORY/enrich.py" --csv "$TEST_FILE" --pcapng "$KISMET_LOG" --output "$ENRICHED_FILE" log "Enrichment took $SECONDS seconds" else log "Skipping enrichment as per --no-enrich flag." fi ATTACHMENTS=() if [ "$LISTENER_ENABLED" -eq 1 ]; then [ -n "$TEST_FILE" ] && [ -f "$TEST_FILE" ] && ATTACHMENTS+=("$TEST_FILE") else [ -f "$ENRICHED_FILE" ] && ATTACHMENTS+=("$ENRICHED_FILE") [ -f "$SSID_METRICS_FILE" ] && ATTACHMENTS+=("$SSID_METRICS_FILE") fi [ -f "$FAILURE_LOG" ] && ATTACHMENTS+=("$FAILURE_LOG") if [ ${#ATTACHMENTS[@]} -eq 0 ]; then warn "No files to attach. Email not sent." else for file in "${ATTACHMENTS[@]}"; do log "Attaching: $file"; done echo "$EMAIL_BODY" | mutt -s "Test ${BOOT_ID} Complete" -a "${ATTACHMENTS[@]}" -- "$RECIPIENT" log "Email sent to $RECIPIENT with attachments." fi sudo kill "$SUDO_KEEPALIVE_PID" SCRIPT_END=$(date +%s) log "Full test cycle completed in $((SCRIPT_END - SCRIPT_START)) seconds"