[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
546
docs/research/issue-2121-netns-stress.sh
Normal file
546
docs/research/issue-2121-netns-stress.sh
Normal file
@@ -0,0 +1,546 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Reproducible, isolated network stress checks for MoshCatty against the
|
||||
# distribution-provided mosh-server. The script only changes links inside two
|
||||
# temporary network namespaces; it does not touch the host's default network.
|
||||
|
||||
MODE=${1:-quick}
|
||||
MOSHCATTY_BIN=${MOSHCATTY_BIN:-/root/mosh-client-0.1.7}
|
||||
RUN_ROOT=${RUN_ROOT:-/root/moshcatty-netns-stress}
|
||||
LONG_INPUTS=${LONG_INPUTS:-1800}
|
||||
LONG_LARGE_EVERY=${LONG_LARGE_EVERY:-30}
|
||||
LONG_PROGRESS_EVERY=${LONG_PROGRESS_EVERY:-300}
|
||||
NS_CLIENT="mc2121c$$"
|
||||
NS_SERVER="mc2121s$$"
|
||||
IF_CLIENT="mc${$}c0"
|
||||
IF_SERVER="mc${$}s0"
|
||||
IPV4_CLIENT=10.212.1.1
|
||||
IPV4_SERVER=10.212.1.2
|
||||
IPV6_CLIENT=fd21:21::1
|
||||
IPV6_SERVER=fd21:21::2
|
||||
PORT=60050
|
||||
TMUX_SOCKET="mc2121-$$"
|
||||
CURRENT_SCREEN=
|
||||
CURRENT_CASE_DIR=
|
||||
CURRENT_KEY=
|
||||
PCAP_PID=
|
||||
|
||||
require_root() {
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This test must run as root." >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
require_tools() {
|
||||
local tool
|
||||
for tool in ip tc tmux timeout tcpdump python3 awk grep sed base64 head ps readlink mosh-server; do
|
||||
command -v "$tool" >/dev/null || {
|
||||
echo "Missing required command: $tool" >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
if [[ ! -x ${MOSHCATTY_BIN} ]]; then
|
||||
echo "MoshCatty binary is not executable: ${MOSHCATTY_BIN}" >&2
|
||||
exit 2
|
||||
fi
|
||||
if ! python3 - <<'PY' >/dev/null 2>&1
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESOCB3
|
||||
PY
|
||||
then
|
||||
echo "Missing Python AES-OCB3 support. Install Ubuntu package python3-cryptography." >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup_client() {
|
||||
if tmux -L "${TMUX_SOCKET}" has-session -t mosh 2>/dev/null; then
|
||||
tmux -L "${TMUX_SOCKET}" kill-server 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local ns pid
|
||||
if [[ -n ${PCAP_PID} ]] && kill -0 "${PCAP_PID}" 2>/dev/null; then
|
||||
kill -INT "${PCAP_PID}" 2>/dev/null || true
|
||||
wait "${PCAP_PID}" 2>/dev/null || true
|
||||
PCAP_PID=
|
||||
fi
|
||||
cleanup_client
|
||||
for ns in "${NS_CLIENT}" "${NS_SERVER}"; do
|
||||
if ip netns list | awk '{print $1}' | grep -Fxq "${ns}"; then
|
||||
while read -r pid; do
|
||||
[[ -n ${pid} ]] && kill "${pid}" 2>/dev/null || true
|
||||
done < <(ip netns pids "${ns}" 2>/dev/null || true)
|
||||
ip netns delete "${ns}" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
setup_namespaces() {
|
||||
mkdir -p "${RUN_ROOT}"
|
||||
ip netns add "${NS_CLIENT}"
|
||||
ip netns add "${NS_SERVER}"
|
||||
ip link add "${IF_CLIENT}" type veth peer name "${IF_SERVER}"
|
||||
ip link set "${IF_CLIENT}" netns "${NS_CLIENT}"
|
||||
ip link set "${IF_SERVER}" netns "${NS_SERVER}"
|
||||
|
||||
ip -n "${NS_CLIENT}" link set lo up
|
||||
ip -n "${NS_SERVER}" link set lo up
|
||||
ip -n "${NS_CLIENT}" addr add "${IPV4_CLIENT}/30" dev "${IF_CLIENT}"
|
||||
ip -n "${NS_SERVER}" addr add "${IPV4_SERVER}/30" dev "${IF_SERVER}"
|
||||
ip -n "${NS_CLIENT}" -6 addr add "${IPV6_CLIENT}/64" dev "${IF_CLIENT}" nodad
|
||||
ip -n "${NS_SERVER}" -6 addr add "${IPV6_SERVER}/64" dev "${IF_SERVER}" nodad
|
||||
ip -n "${NS_CLIENT}" link set "${IF_CLIENT}" up
|
||||
ip -n "${NS_SERVER}" link set "${IF_SERVER}" up
|
||||
|
||||
ip netns exec "${NS_CLIENT}" ping -c 1 -W 2 "${IPV4_SERVER}" >/dev/null
|
||||
ip netns exec "${NS_CLIENT}" ping -6 -c 1 -W 2 "${IPV6_SERVER}" >/dev/null
|
||||
}
|
||||
|
||||
set_netem() {
|
||||
local ns=$1
|
||||
local iface=$2
|
||||
shift 2
|
||||
ip netns exec "${ns}" tc qdisc replace dev "${iface}" root netem "$@"
|
||||
}
|
||||
|
||||
clear_netem() {
|
||||
local ns=$1
|
||||
local iface=$2
|
||||
ip netns exec "${ns}" tc qdisc delete dev "${iface}" root 2>/dev/null || true
|
||||
}
|
||||
|
||||
set_mtu() {
|
||||
local mtu=$1
|
||||
ip -n "${NS_CLIENT}" link set "${IF_CLIENT}" mtu "${mtu}"
|
||||
ip -n "${NS_SERVER}" link set "${IF_SERVER}" mtu "${mtu}"
|
||||
}
|
||||
|
||||
remove_ipv4_addresses() {
|
||||
ip -n "${NS_CLIENT}" -4 addr delete "${IPV4_CLIENT}/30" dev "${IF_CLIENT}"
|
||||
ip -n "${NS_SERVER}" -4 addr delete "${IPV4_SERVER}/30" dev "${IF_SERVER}"
|
||||
}
|
||||
|
||||
restore_ipv4_addresses() {
|
||||
ip -n "${NS_CLIENT}" -4 addr add "${IPV4_CLIENT}/30" dev "${IF_CLIENT}"
|
||||
ip -n "${NS_SERVER}" -4 addr add "${IPV4_SERVER}/30" dev "${IF_SERVER}"
|
||||
}
|
||||
|
||||
start_session() {
|
||||
local name=$1
|
||||
local host=$2
|
||||
local timeout_seconds=${3:-300}
|
||||
local width=${4:-100}
|
||||
local height=${5:-30}
|
||||
local case_dir="${RUN_ROOT}/${name}"
|
||||
local server_output key
|
||||
|
||||
cleanup_client
|
||||
rm -rf "${case_dir}"
|
||||
mkdir -p "${case_dir}"
|
||||
CURRENT_CASE_DIR="${case_dir}"
|
||||
server_output=$(ip netns exec "${NS_SERVER}" env LANG=C.UTF-8 TERM=xterm-256color \
|
||||
mosh-server new -s -i "${host}" -p "${PORT}" -l LANG=C.UTF-8 2>&1)
|
||||
key=$(printf '%s\n' "${server_output}" | awk '$1 == "MOSH" && $2 == "CONNECT" { print $4; exit }')
|
||||
if [[ -z ${key} ]]; then
|
||||
echo "${name}: mosh-server did not return a session key" >&2
|
||||
return 1
|
||||
fi
|
||||
CURRENT_KEY=${key}
|
||||
printf '%s\n' "${server_output}" \
|
||||
| sed -E 's/^(MOSH CONNECT [0-9]+) [^[:space:]]+$/\1 [REDACTED]/' \
|
||||
>"${case_dir}/server.log"
|
||||
|
||||
CURRENT_SCREEN="${case_dir}/client.screen"
|
||||
ip netns exec "${NS_CLIENT}" env \
|
||||
TERM=xterm-256color \
|
||||
LANG=C.UTF-8 \
|
||||
MOSH_KEY="${key}" \
|
||||
MOSH_NO_TERM_INIT=1 \
|
||||
tmux -L "${TMUX_SOCKET}" new-session -d -x "${width}" -y "${height}" -s mosh \
|
||||
"exec timeout --signal=TERM ${timeout_seconds}s ${MOSHCATTY_BIN} ${host} ${PORT}"
|
||||
tmux -L "${TMUX_SOCKET}" pipe-pane -o -t mosh "cat >>${case_dir}/client.raw"
|
||||
sleep 1
|
||||
}
|
||||
|
||||
start_ipv6_capture() {
|
||||
local pcap_file=$1
|
||||
rm -f "${pcap_file}" "${pcap_file}.log"
|
||||
ip netns exec "${NS_CLIENT}" \
|
||||
tcpdump -U -i "${IF_CLIENT}" -s 0 -w "${pcap_file}" \
|
||||
"ip6 and udp port ${PORT}" >"${pcap_file}.log" 2>&1 &
|
||||
PCAP_PID=$!
|
||||
sleep 0.5
|
||||
}
|
||||
|
||||
stop_capture() {
|
||||
if [[ -n ${PCAP_PID} ]]; then
|
||||
kill -INT "${PCAP_PID}" 2>/dev/null || true
|
||||
wait "${PCAP_PID}" 2>/dev/null || true
|
||||
PCAP_PID=
|
||||
fi
|
||||
}
|
||||
|
||||
send_command() {
|
||||
tmux -L "${TMUX_SOCKET}" send-keys -t mosh -l -- "$1"
|
||||
tmux -L "${TMUX_SOCKET}" send-keys -t mosh Enter
|
||||
}
|
||||
|
||||
wait_for_marker() {
|
||||
local marker=$1
|
||||
local timeout_seconds=${2:-60}
|
||||
local deadline=$((SECONDS + timeout_seconds))
|
||||
while (( SECONDS < deadline )); do
|
||||
if ! tmux -L "${TMUX_SOCKET}" has-session -t mosh 2>/dev/null; then
|
||||
echo "Client exited before marker appeared: ${marker}" >&2
|
||||
return 1
|
||||
fi
|
||||
tmux -L "${TMUX_SOCKET}" capture-pane -p -t mosh >"${CURRENT_SCREEN}"
|
||||
if grep -Fq "${marker}" "${CURRENT_SCREEN}"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
echo "Timed out waiting for marker: ${marker}" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
finish_session() {
|
||||
send_command "exit"
|
||||
local deadline=$((SECONDS + 15))
|
||||
while tmux -L "${TMUX_SOCKET}" has-session -t mosh 2>/dev/null && (( SECONDS < deadline )); do
|
||||
sleep 0.2
|
||||
done
|
||||
if tmux -L "${TMUX_SOCKET}" has-session -t mosh 2>/dev/null; then
|
||||
echo "Client did not exit cleanly" >&2
|
||||
return 1
|
||||
fi
|
||||
PORT=$((PORT + 1))
|
||||
}
|
||||
|
||||
assert_exact_sequence() {
|
||||
local file=$1
|
||||
local prefix=$2
|
||||
local expected=$3
|
||||
local i line
|
||||
local -a lines
|
||||
mapfile -t lines <"${file}"
|
||||
if [[ ${#lines[@]} -ne ${expected} ]]; then
|
||||
echo "Expected ${expected} executions in ${file}, found ${#lines[@]}" >&2
|
||||
return 1
|
||||
fi
|
||||
for i in $(seq 1 "${expected}"); do
|
||||
printf -v line '%s:%03d' "${prefix}" "${i}"
|
||||
if [[ ${lines[i - 1]} != "${line}" ]]; then
|
||||
echo "Unexpected execution ${i}: ${lines[i - 1]}" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
verify_ipv6_capture() {
|
||||
local pcap_file=$1
|
||||
local key=$2
|
||||
python3 - "${pcap_file}" 3<<<"${key}" <<'PY'
|
||||
import base64
|
||||
import collections
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import ipaddress
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESOCB3
|
||||
|
||||
path = sys.argv[1]
|
||||
with os.fdopen(3) as key_pipe:
|
||||
key = base64.b64decode(key_pipe.read().strip() + "==")
|
||||
cipher = AESOCB3(key)
|
||||
with open(path, "rb") as handle:
|
||||
header = handle.read(24)
|
||||
if len(header) != 24:
|
||||
raise SystemExit("pcap capture is empty")
|
||||
magic = header[:4]
|
||||
if magic in (b"\xd4\xc3\xb2\xa1", b"\x4d\x3c\xb2\xa1"):
|
||||
endian = "<"
|
||||
elif magic in (b"\xa1\xb2\xc3\xd4", b"\xa1\xb2\x3c\x4d"):
|
||||
endian = ">"
|
||||
else:
|
||||
raise SystemExit("unsupported pcap format")
|
||||
|
||||
packets = 0
|
||||
max_ipv6_length = 0
|
||||
fragment_headers = 0
|
||||
server_packets = 0
|
||||
server_udp_payload = 0
|
||||
server_address = ipaddress.IPv6Address("fd21:21::2").packed
|
||||
fragment_groups = collections.defaultdict(set)
|
||||
while True:
|
||||
record = handle.read(16)
|
||||
if not record:
|
||||
break
|
||||
if len(record) != 16:
|
||||
raise SystemExit("truncated pcap record")
|
||||
_, _, captured, original = struct.unpack(endian + "IIII", record)
|
||||
frame = handle.read(captured)
|
||||
if len(frame) != captured:
|
||||
raise SystemExit("truncated pcap packet")
|
||||
if len(frame) < 54 or frame[12:14] != b"\x86\xdd":
|
||||
continue
|
||||
packets += 1
|
||||
ipv6_length = original - 14
|
||||
max_ipv6_length = max(max_ipv6_length, ipv6_length)
|
||||
if frame[20] == 44:
|
||||
fragment_headers += 1
|
||||
if frame[20] == 17 and frame[22:38] == server_address and len(frame) >= 62:
|
||||
udp_length = struct.unpack("!H", frame[58:60])[0]
|
||||
server_packets += 1
|
||||
server_udp_payload += max(0, udp_length - 8)
|
||||
datagram = frame[62 : 62 + udp_length - 8]
|
||||
if len(datagram) >= 24:
|
||||
nonce = b"\x00" * 4 + datagram[:8]
|
||||
try:
|
||||
plaintext = cipher.decrypt(nonce, datagram[8:], b"")
|
||||
except Exception:
|
||||
continue
|
||||
if len(plaintext) >= 14:
|
||||
instruction_id = int.from_bytes(plaintext[4:12], "big")
|
||||
fragment_num = int.from_bytes(plaintext[12:14], "big") & 0x7FFF
|
||||
fragment_groups[instruction_id].add(fragment_num)
|
||||
|
||||
if server_packets < 2 or server_udp_payload <= 1280:
|
||||
raise SystemExit(
|
||||
"capture did not prove application-level splitting: "
|
||||
f"server_packets={server_packets}, server_udp_payload={server_udp_payload}"
|
||||
)
|
||||
if max_ipv6_length > 1280:
|
||||
raise SystemExit(f"IPv6 packet exceeded MTU 1280: {max_ipv6_length}")
|
||||
if fragment_headers:
|
||||
raise SystemExit(f"IPv6 Fragment Headers observed: {fragment_headers}")
|
||||
largest_fragment_group = max((len(group) for group in fragment_groups.values()), default=0)
|
||||
if largest_fragment_group < 2:
|
||||
raise SystemExit("capture did not contain a multi-fragment Mosh instruction")
|
||||
print(
|
||||
"pcap verified: "
|
||||
f"packets={packets}, server_packets={server_packets}, "
|
||||
f"server_udp_payload={server_udp_payload}, "
|
||||
f"max_ipv6_packet={max_ipv6_length}, fragment_headers=0, "
|
||||
f"largest_mosh_fragment_group={largest_fragment_group}"
|
||||
)
|
||||
PY
|
||||
}
|
||||
|
||||
client_rss_kib() {
|
||||
local pid exe
|
||||
while read -r pid; do
|
||||
[[ -n ${pid} ]] || continue
|
||||
exe=$(readlink -f "/proc/${pid}/exe" 2>/dev/null || true)
|
||||
if [[ ${exe} == "${MOSHCATTY_BIN}" ]]; then
|
||||
ps -o rss= -p "${pid}" | awk '{print $1}'
|
||||
return 0
|
||||
fi
|
||||
done < <(ip netns pids "${NS_CLIENT}")
|
||||
echo "Could not find the running MoshCatty client" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
test_upstream_loss_baseline() {
|
||||
echo "[quick 1/4] upstream-published 100 ms RTT and 29% loss in each direction"
|
||||
set_mtu 1500
|
||||
set_netem "${NS_CLIENT}" "${IF_CLIENT}" delay 50ms loss 29%
|
||||
set_netem "${NS_SERVER}" "${IF_SERVER}" delay 50ms loss 29%
|
||||
start_session upstream_loss "${IPV4_SERVER}" 240
|
||||
local execution_log="${CURRENT_CASE_DIR}/executions.log"
|
||||
local i marker
|
||||
for i in $(seq 1 10); do
|
||||
printf -v marker 'LOSS_OK:%03d' "${i}"
|
||||
send_command "printf 'LOSS_OK:%03d\\n' ${i} >>${execution_log}; printf '%s:%03d\\n' LOSS_OK ${i}"
|
||||
wait_for_marker "${marker}" 90
|
||||
done
|
||||
assert_exact_sequence "${execution_log}" LOSS_OK 10
|
||||
finish_session
|
||||
echo "PASS upstream loss baseline (10/10 exactly once)"
|
||||
}
|
||||
|
||||
test_asymmetric_impairment() {
|
||||
echo "[quick 2/4] asymmetric latency, loss, duplication, and reordering"
|
||||
set_mtu 1500
|
||||
set_netem "${NS_CLIENT}" "${IF_CLIENT}" delay 300ms 60ms distribution normal loss 5% duplicate 2% reorder 10% 50%
|
||||
set_netem "${NS_SERVER}" "${IF_SERVER}" delay 450ms 80ms distribution normal loss 12% duplicate 3% reorder 15% 50%
|
||||
start_session asymmetric "${IPV4_SERVER}" 180
|
||||
local execution_log="${CURRENT_CASE_DIR}/executions.log"
|
||||
local i marker
|
||||
for i in $(seq 1 10); do
|
||||
printf -v marker 'NET_OK:%03d' "${i}"
|
||||
send_command "printf 'NET_OK:%03d\\n' ${i} >>${execution_log}; printf '%s:%03d\\n' NET_OK ${i}"
|
||||
wait_for_marker "${marker}" 60
|
||||
done
|
||||
assert_exact_sequence "${execution_log}" NET_OK 10
|
||||
finish_session
|
||||
echo "PASS asymmetric impairment (10/10 exactly once)"
|
||||
}
|
||||
|
||||
test_long_outage() {
|
||||
echo "[quick 3/4] queued input across a 65-second total outage"
|
||||
set_mtu 1500
|
||||
set_netem "${NS_CLIENT}" "${IF_CLIENT}" delay 80ms 10ms loss 1%
|
||||
set_netem "${NS_SERVER}" "${IF_SERVER}" delay 120ms 15ms loss 2%
|
||||
start_session outage65 "${IPV4_SERVER}" 210
|
||||
send_command "printf '%s%s\\n' BEFORE_ OUTAGE"
|
||||
wait_for_marker BEFORE_OUTAGE 30
|
||||
|
||||
set_netem "${NS_CLIENT}" "${IF_CLIENT}" loss 100%
|
||||
set_netem "${NS_SERVER}" "${IF_SERVER}" loss 100%
|
||||
local execution_log="${CURRENT_CASE_DIR}/executions.log"
|
||||
send_command "printf '%s\\n' OUTAGE_QUEUED >>${execution_log}; printf '%s%s\\n' QUEUED_ INPUT_OK"
|
||||
sleep 65
|
||||
|
||||
set_netem "${NS_CLIENT}" "${IF_CLIENT}" delay 80ms 10ms loss 1%
|
||||
set_netem "${NS_SERVER}" "${IF_SERVER}" delay 120ms 15ms loss 2%
|
||||
wait_for_marker QUEUED_INPUT_OK 10
|
||||
if [[ $(grep -Fxc OUTAGE_QUEUED "${execution_log}") -ne 1 ]]; then
|
||||
echo "Input queued during the outage was not executed exactly once" >&2
|
||||
return 1
|
||||
fi
|
||||
send_command "printf '%s%s\\n' AFTER_ OUTAGE"
|
||||
wait_for_marker AFTER_OUTAGE 10
|
||||
finish_session
|
||||
echo "PASS 65-second outage recovery with queued input preserved exactly once"
|
||||
}
|
||||
|
||||
test_ipv6_minimum_mtu() {
|
||||
echo "[quick 4/4] IPv6 minimum MTU, large incompressible screen, and packet capture"
|
||||
clear_netem "${NS_CLIENT}" "${IF_CLIENT}"
|
||||
clear_netem "${NS_SERVER}" "${IF_SERVER}"
|
||||
set_mtu 1280
|
||||
remove_ipv4_addresses
|
||||
set_netem "${NS_CLIENT}" "${IF_CLIENT}" delay 120ms 20ms loss 2% duplicate 1%
|
||||
set_netem "${NS_SERVER}" "${IF_SERVER}" delay 180ms 30ms loss 4% reorder 5% 50%
|
||||
local pcap_file="${RUN_ROOT}/ipv6-mtu1280.pcap"
|
||||
start_ipv6_capture "${pcap_file}"
|
||||
start_session ipv6_mtu1280 "${IPV6_SERVER}" 240 200 100
|
||||
send_command "head -c 12000 /dev/urandom | base64; printf '%s%s\\n' IPV6_ MTU_OK"
|
||||
wait_for_marker IPV6_MTU_OK 90
|
||||
sleep 3
|
||||
finish_session
|
||||
sleep 1
|
||||
stop_capture
|
||||
verify_ipv6_capture "${pcap_file}" "${CURRENT_KEY}"
|
||||
CURRENT_KEY=
|
||||
restore_ipv4_addresses
|
||||
echo "PASS IPv6 MTU 1280 with application-level splitting and no network fragmentation"
|
||||
}
|
||||
|
||||
test_long_asymmetric_pressure() {
|
||||
echo "[long] ${LONG_INPUTS}-second asymmetric network pressure with one input per second"
|
||||
set_mtu 1500
|
||||
set_netem "${NS_CLIENT}" "${IF_CLIENT}" delay 250ms 60ms distribution normal loss 1% duplicate 1% reorder 5% 50%
|
||||
set_netem "${NS_SERVER}" "${IF_SERVER}" delay 450ms 100ms distribution normal loss 3% duplicate 2% reorder 10% 50%
|
||||
start_session long30m "${IPV4_SERVER}" 2700
|
||||
|
||||
local execution_log="${CURRENT_CASE_DIR}/executions.log"
|
||||
local i marker payload target delay rss baseline_rss final_rss allowed_growth allowed_final
|
||||
local monotonic_growth=1
|
||||
local started=${SECONDS}
|
||||
local -a rss_samples=()
|
||||
for i in $(seq 1 "${LONG_INPUTS}"); do
|
||||
payload=
|
||||
if (( i % LONG_LARGE_EVERY == 0 )); then
|
||||
payload="head -c 12000 /dev/urandom | base64; "
|
||||
fi
|
||||
send_command "${payload}printf 'LONG_OK:%03d\\n' ${i} >>${execution_log}; printf '%s:%03d\\n' LONG_OK ${i}"
|
||||
|
||||
if (( i % LONG_PROGRESS_EVERY == 0 || i == LONG_INPUTS )); then
|
||||
rss=$(client_rss_kib)
|
||||
rss_samples+=("${rss}")
|
||||
if [[ -z ${baseline_rss:-} ]]; then
|
||||
baseline_rss=${rss}
|
||||
fi
|
||||
echo " progress: ${i}/${LONG_INPUTS} inputs queued, client RSS ${rss} KiB"
|
||||
fi
|
||||
|
||||
target=$((started + i))
|
||||
delay=$((target - SECONDS))
|
||||
if (( delay > 0 )); then
|
||||
sleep "${delay}"
|
||||
fi
|
||||
done
|
||||
|
||||
printf -v marker 'LONG_OK:%03d' "${LONG_INPUTS}"
|
||||
wait_for_marker "${marker}" 300
|
||||
assert_exact_sequence "${execution_log}" LONG_OK "${LONG_INPUTS}"
|
||||
|
||||
final_rss=$(client_rss_kib)
|
||||
allowed_growth=$((baseline_rss / 4))
|
||||
if (( allowed_growth < 32768 )); then
|
||||
allowed_growth=32768
|
||||
fi
|
||||
allowed_final=$((baseline_rss + allowed_growth))
|
||||
if (( final_rss > allowed_final )); then
|
||||
echo "Client RSS grew beyond the allowed bound: baseline=${baseline_rss}, final=${final_rss}, allowed=${allowed_final} KiB" >&2
|
||||
return 1
|
||||
fi
|
||||
if (( ${#rss_samples[@]} >= 4 )); then
|
||||
for ((i = 1; i < ${#rss_samples[@]}; i++)); do
|
||||
if (( rss_samples[i] <= rss_samples[i - 1] )); then
|
||||
monotonic_growth=0
|
||||
break
|
||||
fi
|
||||
done
|
||||
if (( monotonic_growth == 1 && rss_samples[${#rss_samples[@]} - 1] - rss_samples[0] > 8192 )); then
|
||||
echo "Client RSS grew monotonically by more than 8 MiB: ${rss_samples[*]} KiB" >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
clear_netem "${NS_CLIENT}" "${IF_CLIENT}"
|
||||
clear_netem "${NS_SERVER}" "${IF_SERVER}"
|
||||
send_command "printf '%s%s\\n' POST_ PRESSURE_OK"
|
||||
wait_for_marker POST_PRESSURE_OK 10
|
||||
finish_session
|
||||
echo " RSS samples (KiB): ${rss_samples[*]}"
|
||||
echo "PASS ${LONG_INPUTS}-second asymmetric pressure (${LONG_INPUTS}/${LONG_INPUTS} exactly once, recovery under 10 seconds)"
|
||||
}
|
||||
|
||||
main() {
|
||||
require_root
|
||||
require_tools
|
||||
setup_namespaces
|
||||
|
||||
case "${MODE}" in
|
||||
quick)
|
||||
test_upstream_loss_baseline
|
||||
test_asymmetric_impairment
|
||||
test_long_outage
|
||||
test_ipv6_minimum_mtu
|
||||
;;
|
||||
long)
|
||||
test_long_asymmetric_pressure
|
||||
;;
|
||||
ipv6)
|
||||
test_ipv6_minimum_mtu
|
||||
;;
|
||||
outage)
|
||||
test_long_outage
|
||||
;;
|
||||
all)
|
||||
test_upstream_loss_baseline
|
||||
test_asymmetric_impairment
|
||||
test_long_outage
|
||||
test_ipv6_minimum_mtu
|
||||
test_long_asymmetric_pressure
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [quick|long|ipv6|outage|all]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "All requested network namespace stress checks passed."
|
||||
echo "Logs: ${RUN_ROOT}"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user