Files
MeshCore/tools/hil/ethernet_ssh_flutter_bench.sh
T

184 lines
8.3 KiB
Bash

#!/usr/bin/env bash
# Isolated Linux client for the explicitly opt-in Flutter SSH-over-radio test.
set -euo pipefail
endpoint=''; bench_dir=''; workspace=''; flutter=''; private_key=''; fingerprint=''
prefix=''; account=''; test_name=rf; test_timeout=210; phase_timeout=45
die() { printf '%s\n' "$*" >&2; exit 2; }
while (($#)); do
(($# >= 2)) || die 'Every option requires a value.'
case "$1" in
--endpoint) endpoint=$2 ;; --bench-dir) bench_dir=$2 ;;
--workspace) workspace=$2 ;; --flutter) flutter=$2 ;;
--key) private_key=$2 ;; --fingerprint) fingerprint=$2 ;;
--prefix) prefix=$2 ;; --account) account=$2 ;;
--test-name) test_name=$2 ;;
--phase-timeout)
[[ "$2" =~ ^[0-9]{1,2}$ ]] && ((10#$2 >= 5 && 10#$2 <= 60)) \
|| die '--phase-timeout must be 5..60 seconds.'
phase_timeout=$((10#$2)) ;;
--test-timeout)
[[ "$2" =~ ^[0-9]{1,3}$ ]] && ((10#$2 >= 1 && 10#$2 <= 180)) \
|| die '--test-timeout must be 1..180 seconds.'
test_timeout=$((10#$2)) ;;
*) die "Unknown option: $1" ;;
esac
shift 2
done
[[ "$test_name" == rf || "$test_name" == speed ]] || die '--test-name must be rf or speed.'
test_file="test/services/lora_ssh_${test_name}_test.dart"
[[ ${EUID:-$(id -u)} == 0 ]] || die 'Root is required for the isolated namespace/TAP.'
[[ "$prefix" =~ ^ethflutter-[a-z0-9][a-z0-9-]{0,19}$ ]] || die 'Use a unique ethflutter- prefix.'
[[ "$account" =~ ^[a-z_][a-z0-9_-]*$ ]] || die 'Specify an unprivileged test account.'
[[ $(id -u "$account") -ne 0 ]] || die 'The Flutter test account must not be root.'
for directory in "$bench_dir" "$workspace"; do
[[ "$directory" == /* && "$directory" != / && -d "$directory" ]] || die 'Use existing absolute directories.'
done
[[ "$flutter" == /* && -x "$flutter" && "$private_key" == /* && -f "$private_key" ]] || die 'Use explicit Flutter/key paths.'
[[ -f "$workspace/$test_file" ]] || die 'The selected opt-in SSH backend test is missing.'
[[ "$fingerprint" =~ ^SHA256:[A-Za-z0-9+/]{43}$ ]] || die 'Use the expected OpenSSH SHA-256 fingerprint.'
runuser -u "$account" -- test -r "$private_key" || die 'The test account cannot read its selected key.'
[[ ! -e "/var/run/netns/$prefix" ]] || die 'Refusing to reuse an existing namespace.'
python3 - "$endpoint" <<'PY'
import ipaddress, sys
from urllib.parse import urlparse
u = urlparse(sys.argv[1])
try:
host = ipaddress.IPv4Address(u.hostname)
valid = (u.scheme == 'tcp' and u.port == 5003 and not u.username and not u.password
and u.path in ('', '/') and not u.query and not u.fragment
and not host.is_multicast and not host.is_unspecified)
except ValueError:
valid = False
if not valid:
raise SystemExit('Use an explicit numeric IPv4 tcp://RADIO:5003 endpoint.')
PY
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
run_dir=$(mktemp -d -- "$bench_dir/ethernet-flutter.XXXXXXXX")
chown "$account": "$run_dir"
chmod 700 -- "$run_dir"
# Keep evidence readable by the selected account even if sudo inherited 077.
# The account-owned parent remains private (0700); no keys are copied here.
umask 022
created=0; inode=''; relay_pid=''; test_pid=''; monitor_pid=''; tcp_started=0; tcp_finished=0
capture_tcp() {
local phase=$1 observed=''
observed=$(stat -Lc '%d:%i' -- "/var/run/netns/$prefix" 2>/dev/null) || return 1
[[ -n "$inode" && "$observed" == "$inode" ]] || return 1
date -u +%Y-%m-%dT%H:%M:%SZ > "$run_dir/tcp-$phase.utc"
ip netns exec "$prefix" cat /proc/net/snmp > "$run_dir/tcp-$phase.snmp" \
2> "$run_dir/tcp-$phase.error"
}
finish_tcp() {
((tcp_started && !tcp_finished)) || return 0
tcp_finished=1
capture_tcp after || true
python3 "$script_dir/ethernet_tcp_stats.py" --directory "$run_dir" \
> "$run_dir/tcp-stats.error" 2>&1 || true
}
sample_tcp() {
local remaining=300 sample lines observed
while ((remaining > 0)) && kill -0 "$test_pid" 2>/dev/null; do
observed=$(stat -Lc '%d:%i' -- "/var/run/netns/$prefix" 2>/dev/null) || break
[[ "$observed" == "$inode" ]] || break
sample=$(printf '\nUTC %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
timeout --kill-after=1s 2s ip netns exec "$prefix" ss -tin 2>&1 | head -n 12 || true)
printf '%s\n' "$sample" | head -n "$remaining" >> "$run_dir/tcp-ss.txt"
lines=$(printf '%s\n' "$sample" | wc -l)
remaining=$((remaining - lines))
sleep 1
done
}
cleanup() {
local result=$? observed='' child parent
trap - EXIT INT TERM HUP
set +e
finish_tcp
for child in "$test_pid" "$monitor_pid" "$relay_pid"; do
if [[ "$child" =~ ^[0-9]+$ ]]; then
parent=$(awk '/^PPid:/ {print $2}' "/proc/$child/status" 2>/dev/null)
[[ "$parent" == "$$" ]] && kill -TERM "$child" 2>/dev/null
fi
done
if ((created)); then
observed=$(stat -Lc '%d:%i' -- "/var/run/netns/$prefix" 2>/dev/null)
if [[ -n "$inode" && "$observed" == "$inode" ]]; then
while read -r child; do
[[ "$child" =~ ^[0-9]+$ ]] && kill -TERM "$child" 2>/dev/null
done < <(ip netns pids "$prefix")
sleep 0.3
observed=$(stat -Lc '%d:%i' -- "/var/run/netns/$prefix" 2>/dev/null)
if [[ "$observed" == "$inode" ]]; then
while read -r child; do
[[ "$child" =~ ^[0-9]+$ ]] && kill -KILL "$child" 2>/dev/null
done < <(ip netns pids "$prefix")
ip netns delete "$prefix"
fi
fi
fi
# Reap only our explicit children. A relay still connecting outside the
# namespace must not make cleanup wait forever after ignoring SIGTERM.
for child in "$test_pid" "$monitor_pid" "$relay_pid"; do
[[ "$child" =~ ^[0-9]+$ ]] || continue
parent=$(awk '/^PPid:/ {print $2}' "/proc/$child/status" 2>/dev/null)
if [[ "$parent" == "$$" ]]; then
for _ in {1..20}; do
kill -0 "$child" 2>/dev/null || break
sleep 0.1
done
parent=$(awk '/^PPid:/ {print $2}' "/proc/$child/status" 2>/dev/null)
[[ "$parent" == "$$" ]] && kill -KILL "$child" 2>/dev/null
fi
wait "$child" 2>/dev/null
done
printf 'exit=%s\n' "$result" >> "$run_dir/result.txt"
printf 'Flutter RF artifacts: %s\n' "$run_dir"
exit "$result"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM HUP
ip netns add "$prefix"; created=1
inode=$(stat -Lc '%d:%i' -- "/var/run/netns/$prefix")
ip -n "$prefix" link set lo up
ip netns exec "$prefix" sysctl -qw net.ipv6.conf.all.disable_ipv6=1
ip netns exec "$prefix" sysctl -qw net.ipv6.conf.default.disable_ipv6=1
python3 "$script_dir/ethernet_tap_relay.py" --endpoint "$endpoint" \
--netns "/var/run/netns/$prefix" --interface lora0 --duration 240 \
--status-file "$run_dir/relay.json" > "$run_dir/relay.log" 2>&1 &
relay_pid=$!
for _ in {1..60}; do
kill -0 "$relay_pid" || die 'TAP relay exited; inspect relay.log.'
ip -n "$prefix" link show dev lora0 >/dev/null 2>&1 && break
sleep 0.2
done
ip -n "$prefix" link set lora0 address 02:90:05:00:00:01 mtu 230 up
ip -n "$prefix" address add 10.90.5.1/24 dev lora0
ip -n "$prefix" route show > "$run_dir/routes.txt"
cd -- "$workspace"
tcp_started=1
capture_tcp before || true
timeout --kill-after=5s "${test_timeout}s" ip netns exec "$prefix" runuser -u "$account" -- env \
LORA_SSH_RF_KEY="$private_key" EXPECTED_HOST_FINGERPRINT_SHA256="$fingerprint" \
LORA_SSH_RF_PORT=2222 LORA_SSH_RF_USER=mikec \
LORA_SSH_SPEED_REPORT="$run_dir/speed.json" LORA_SSH_SPEED_BYTES=8192 \
LORA_SSH_SPEED_TIMEOUT_SECONDS="$phase_timeout" LORA_SSH_SPEED_TIMEOUT="$phase_timeout" \
"$flutter" test --no-pub --reporter expanded "$test_file" \
> "$run_dir/flutter.txt" 2>&1 &
test_pid=$!
sample_tcp &
monitor_pid=$!
test_result=0
wait "$test_pid" || test_result=$?
test_pid=''
if [[ "$monitor_pid" =~ ^[0-9]+$ ]]; then
kill -TERM "$monitor_pid" 2>/dev/null || true
wait "$monitor_pid" 2>/dev/null || true
fi
monitor_pid=''
finish_tcp
printf 'scenario=%s\n' "$test_name" > "$run_dir/result.txt"
if ((test_result == 0)); then
printf 'PASS: real Flutter SSH backend test completed through the isolated radio TAP.\n' >> "$run_dir/result.txt"
fi
exit "$test_result"