374 lines
13 KiB
Bash
Executable file
374 lines
13 KiB
Bash
Executable file
#!/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)
|
|
|
|
# CSV setup
|
|
TEST_HEADER="StartTimestamp,EndTimestamp,Link,Level,Noise,BSSID,TX Bitrate,RX Bitrate,$(speedtest --csv-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 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"
|