184 lines
8.3 KiB
Bash
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"
|