diff --git a/scripts/test_udp_tunnel.sh b/scripts/test_udp_tunnel.sh new file mode 100644 index 0000000..36ad9a4 --- /dev/null +++ b/scripts/test_udp_tunnel.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# test_udp_tunnel.sh — e2e-тест UDP-туннеля (§10 п.8): +# хост-файл → udp_probe send → UDP → udp_gw -l → transmitter → ЭФИР → +# receiver → udp_gw -d → UDP → udp_probe recv → файл → md5-сверка +# Использование: scripts/test_udp_tunnel.sh [ipA] [ipB] [размер_КБ] +# По умолчанию: адреса из toolchain.env, 64 КБ, симплекс 915 МГц. +# +# ВНИМАНИЕ: правило №7 (кабель+аттенюатор) для udp_gw не применимо — шлюз не +# трогает PHY ни на байт (record-слой поверх уже принятого байтового потока), +# его логика проверена `make gwtest` + loopback на плате. Прогон штатно идёт +# на антеннах: TXGAIN=-20 scripts/test_udp_tunnel.sh +set -euo pipefail +source "$(dirname "$0")/../toolchain.env" + +A="${1:-$PLUTO_A}" # передатчик +B="${2:-$PLUTO_B}" # приёмник +SIZE_KB="${3:-64}" + +FREQ=915000000 +RATE=1920000 # НЕ поднимать: бюджет CPU, docs/BENCHMARK.md +BW=1500000 +TXGAIN="${TXGAIN:--30}" # OTA: переопредели, напр. TXGAIN=-20 scripts/test_udp_tunnel.sh +FEC="${FEC:-rs8}" +PAUSE="${PAUSE:-3000}" # ≤1 МБ — 3000 чист (§12.6); длинные прогоны — 4000 +GROUPFEC="${GROUPFEC:-1}" +[ "$GROUPFEC" = 1 ] && GFLAG="-G" || GFLAG="" + +PORT_A="${PORT_A:-6000}" # udp_gw -l на плате A (вход туннеля) +PORT_B="${PORT_B:-6001}" # udp_probe recv на плате B (выход туннеля) +CHUNK="${CHUNK:-1000}" # размер датаграммы; >1016 Б рекорд рвётся на 2 кадра +PACE_MS="${PACE_MS:-15}" # 1000 Б / 15 мс ≈ 65 КБ/с — с запасом под потолок + # `-G -p 3000` ≈107 КБ/с. На 10 мс (≈100 КБ/с) вход + # упирается в потолок и шлюз штатно дропает. +IDLE="${IDLE:-30}" # сторожевой таймер udp_probe recv. ВАЖНО: тикает + # с момента СТАРТА, не с первой датаграммы → зазор + # старт-B→передача должен быть скриптованным (~4 с), + # а не ручной вставкой команд. Штатное завершение — + # активный killall из шага [5], не по таймауту. + +if command -v sshpass >/dev/null; then + SSH=(sshpass -p "$PLUTO_PASS" ssh -o StrictHostKeyChecking=no) +else + SSH=(ssh) + echo "подсказка: apt install sshpass — и пароль вводить не придётся" +fi +if command -v sshpass >/dev/null; then + SCP=(sshpass -p "$PLUTO_PASS" scp -O -o StrictHostKeyChecking=no) +else + SCP=(scp -O) +fi + +echo "=== [1/6] Тестовый файл ${SIZE_KB} КБ (чанк ${CHUNK} Б, темп ${PACE_MS} мс) ===" +dd if=/dev/urandom of=/tmp/probe_in.bin bs=1024 count="$SIZE_KB" status=none +MD5_SRC=$(md5sum /tmp/probe_in.bin | cut -d' ' -f1) +DGRAMS=$(( (SIZE_KB * 1024 + CHUNK - 1) / CHUNK )) +echo "md5: $MD5_SRC | ожидается датаграмм: $DGRAMS" + +echo "=== [2/6] Заливка файла на Pluto A ($A) ===" +"${SCP[@]}" /tmp/probe_in.bin "root@$A:/tmp/" + +echo "=== [3/6] Старт цепочки на Pluto B ($B): receiver | udp_gw -d → udp_probe recv ===" +# heredoc в `sh -s`: stdin ssh занят скриптом → фоновый ssh не ловит SIGTTIN +# (без этого `ssh ... &` уходит в «Остановлен» при попытке читать терминал). +"${SSH[@]}" "root@$B" 'sh -s' </tmp/rx.log \ + | /tmp/udp_gw -d 127.0.0.1:$PORT_B 2>/tmp/gwd.log & +/tmp/udp_probe recv $PORT_B $IDLE > /tmp/probe_out.bin 2>/tmp/probe_rx.log +killall receiver udp_gw 2>/dev/null || true +EOF +B_PID=$! +sleep 4 # дать приёмнику прогреться до старта TX + +echo "=== [4/6] Передача с Pluto A: udp_probe send → udp_gw -l → transmitter ===" +# mkfifo: у пайпа `$!` — только последний процесс, а гейтвей надо убить отдельно +# (его выход = EOF транзмиттеру → штатное завершение передачи). +"${SSH[@]}" "root@$A" 'sh -s' < /tmp/gw.fifo 2>/tmp/gwl.log & +GWP=\$! +/tmp/transmitter -f $FREQ -r $RATE -b $BW -g $TXGAIN -p $PAUSE \ + -c $FEC $GFLAG -u local: < /tmp/gw.fifo 2>/tmp/tx.log & +TXP=\$! +sleep 1 +/tmp/udp_probe send 127.0.0.1:$PORT_A $CHUNK $PACE_MS \ + < /tmp/probe_in.bin 2>/tmp/probe_tx.log +sleep 2 +kill \$GWP 2>/dev/null || true +wait \$TXP +rm -f /tmp/gw.fifo +EOF + +echo "=== [5/6] Гашение цепочки B ===" +sleep 3 # дохлебать хвост эфира +# udp_probe по SIGTERM печатает статистику и закрывает файл — гасим активно, +# не дожидаясь IDLE (тот же паттерн sleep+kill, что в test_file_link.sh). +"${SSH[@]}" -n "root@$B" "killall udp_probe receiver udp_gw 2>/dev/null || true" || true +wait "$B_PID" 2>/dev/null || true + +echo "=== [6/6] Сверка ===" +MD5_DST=$("${SSH[@]}" -n "root@$B" "md5sum /tmp/probe_out.bin 2>/dev/null | cut -d' ' -f1" || echo "нет_файла") +SIZE_DST=$("${SSH[@]}" -n "root@$B" "wc -c < /tmp/probe_out.bin 2>/dev/null" || echo 0) + +echo "источник: $MD5_SRC ($((SIZE_KB*1024)) байт)" +echo "туннель: $MD5_DST ($SIZE_DST байт)" +echo +echo "--- A: udp_probe send ---"; "${SSH[@]}" -n "root@$A" "cat /tmp/probe_tx.log" || true +echo "--- A: udp_gw -l (вход) ---"; "${SSH[@]}" -n "root@$A" "tail -2 /tmp/gwl.log" || true +echo "--- A: transmitter ---"; "${SSH[@]}" -n "root@$A" "tail -4 /tmp/tx.log" || true +echo "--- B: receiver ---"; "${SSH[@]}" -n "root@$B" "tail -6 /tmp/rx.log" || true +echo "--- B: udp_gw -d (выход) ---";"${SSH[@]}" -n "root@$B" "tail -2 /tmp/gwd.log" || true +echo "--- B: udp_probe recv ---"; "${SSH[@]}" -n "root@$B" "cat /tmp/probe_rx.log" || true +echo + +if [ "$MD5_SRC" = "$MD5_DST" ]; then + echo "РЕЗУЛЬТАТ: OK — туннель прозрачен, $DGRAMS датаграмм доставлены байт-в-байт" +else + echo "РЕЗУЛЬТАТ: FAIL — разбор по счётчикам выше:" + echo " - 'дыр(id)' > 0 или 'ресинк' > 0 при живом потоке → НЕ баг туннеля:" + echo " радиопотери сверх group-FEC 16+2 (на симплексе ARQ нет, техдолг #6)." + echo " Контракт соблюдён: дыры посчитаны, поток рекордов не развалился." + echo " Меры: TXGAIN ближе к −20, антенны разнести/переориентировать." + echo " - 'дропов' > 0 у 'udp_gw -l (вход)' → вход быстрее линка (~107 КБ/с" + echo " на -G -p 3000): подними PACE_MS. Это контрактный дроп, семантика UDP." + echo " - размер 0 и в rx.log 0 кадров → приёмник не работал во время передачи:" + echo " частота/усиление/антенны, либо цепочка B умерла по IDLE (=$IDLE с)." + echo " - размер совпал, md5 нет → искажение полезной нагрузки: техдолг #1/#3." +fi