5baf3f4dc7
Wireshark's built-in HSMS dissector — written by network-protocol authors who don't know us, didn't talk to us, and don't share implementation details with secsgem-py — is a third independent codec for our framing. If they parse our pcap without warnings, our HSMS framing is wire-correct independently of both our internal tests and the secsgem-py interop path. interop/tshark_validate.sh: - Boots secs_server on 127.0.0.1:5099 (away from the demo port) - Captures the loopback wire traffic with tcpdump - Runs secs_client through ~24 transactions plus Separate.req + TCP FIN - Parses the pcap with tshark -V using the HSMS dissector - Asserts: no "Malformed Packet", no "Dissector bug", at least one HSMS frame, expected tokens present (Select.req/rsp, Separate.req, Data message), reports histogram (count by control type + distinct S/F pairs) Result against the demo: 69 HSMS frames dissected, 49 distinct S/F pairs (S01F01..S16F28), all clean. Dockerfile gains tshark + tcpdump. .gitea/workflows/ci.yml gains a `tshark-dissector` job that runs this validator as part of every push to main. README proof table grows to 6 commands. VERIFICATION.md §1a documents a follow-up: round-trip the KAT fixtures through secsgem-py to corroborate that the format codes we used match an independent implementation. Strengthens the KAT proof from "internally consistent" to "confirmed by a second implementer who read the spec without talking to us." Plan: VERIFICATION.md §2. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
138 lines
5.1 KiB
Bash
Executable File
138 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Validate our HSMS framing against Wireshark/tshark's built-in HSMS
|
|
# dissector — an independent third codec.
|
|
#
|
|
# Wireshark's HSMS dissector is written by network-protocol authors
|
|
# who haven't read our code, didn't talk to us, and don't share
|
|
# implementation details with secsgem-py. If they parse our pcap
|
|
# without warnings, our HSMS framing is wire-correct independently of
|
|
# both our internal tests and the secsgem-py interop path.
|
|
#
|
|
# Usage: tshark_validate.sh [--port PORT]
|
|
# Run inside the builder image (which has tshark + tcpdump).
|
|
#
|
|
# Exit codes:
|
|
# 0 — tshark dissected every HSMS frame without errors
|
|
# 1 — malformed packet or dissector bug
|
|
# 2 — script error (binary missing, capture failed, etc.)
|
|
set -euo pipefail
|
|
|
|
PORT=${PORT:-5099}
|
|
SERVER=/app/build/secs_server
|
|
CLIENT=/app/build/secs_client
|
|
[ -x "$SERVER" ] || { echo "FAIL: $SERVER missing — rebuild first"; exit 2; }
|
|
[ -x "$CLIENT" ] || { echo "FAIL: $CLIENT missing — rebuild first"; exit 2; }
|
|
|
|
TMPDIR=$(mktemp -d)
|
|
PCAP="$TMPDIR/secsgem.pcap"
|
|
SERVER_LOG="$TMPDIR/server.log"
|
|
CLIENT_LOG="$TMPDIR/client.log"
|
|
TSHARK_LOG="$TMPDIR/tshark.txt"
|
|
|
|
cleanup() {
|
|
[ -n "${TCPDUMP_PID:-}" ] && kill "$TCPDUMP_PID" 2>/dev/null || true
|
|
[ -n "${SERVER_PID:-}" ] && kill "$SERVER_PID" 2>/dev/null || true
|
|
wait 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
echo "starting tcpdump capture on lo:$PORT → $PCAP"
|
|
tcpdump -i lo -w "$PCAP" "tcp port $PORT" >/dev/null 2>&1 &
|
|
TCPDUMP_PID=$!
|
|
# Give tcpdump a moment to attach to the interface before the server binds.
|
|
sleep 0.5
|
|
|
|
echo "starting secs_server on 127.0.0.1:$PORT"
|
|
$SERVER --port "$PORT" --device 0 \
|
|
--config /app/data/equipment.yaml \
|
|
--state-table /app/data/control_state.yaml \
|
|
--pj-state-table /app/data/process_job_state.yaml \
|
|
--cj-state-table /app/data/control_job_state.yaml \
|
|
> "$SERVER_LOG" 2>&1 &
|
|
SERVER_PID=$!
|
|
sleep 0.5
|
|
|
|
echo "running secs_client (~24 transactions)"
|
|
$CLIENT --host 127.0.0.1 --port "$PORT" --device 0 \
|
|
> "$CLIENT_LOG" 2>&1 || true
|
|
# Let the final separate / TCP teardown land in the pcap. The client
|
|
# sends Separate.req then exits; we need to give tcpdump enough time
|
|
# to flush both the Separate frame and the trailing FIN/ACK pair to
|
|
# the pcap file (tcpdump buffers internally).
|
|
sleep 1.5
|
|
|
|
cleanup
|
|
trap - EXIT
|
|
|
|
# Make sure tcpdump flushed.
|
|
sleep 0.5
|
|
|
|
if [ ! -s "$PCAP" ]; then
|
|
echo "FAIL: empty pcap (capture didn't work?)"
|
|
echo "--- server log ---"; cat "$SERVER_LOG"
|
|
echo "--- client log ---"; cat "$CLIENT_LOG"
|
|
exit 2
|
|
fi
|
|
|
|
echo "parsing $PCAP with tshark + HSMS dissector"
|
|
# -d tcp.port==$PORT,hsms forces the dissector for our chosen port
|
|
# (Wireshark's default is 5000, so a non-default port needs the hint).
|
|
tshark -r "$PCAP" -d "tcp.port==$PORT,hsms" -V > "$TSHARK_LOG" 2>&1
|
|
|
|
if grep -qi 'Malformed Packet' "$TSHARK_LOG"; then
|
|
echo "FAIL: tshark reported Malformed Packet — dissector failed on our frame"
|
|
grep -B2 -A6 'Malformed Packet' "$TSHARK_LOG"
|
|
exit 1
|
|
fi
|
|
if grep -qi 'Dissector bug' "$TSHARK_LOG"; then
|
|
echo "FAIL: Dissector bug reported"
|
|
grep -B2 -A6 'Dissector bug' "$TSHARK_LOG"
|
|
exit 1
|
|
fi
|
|
|
|
frames=$(grep -c 'High-speed SECS Message Service' "$TSHARK_LOG" || true)
|
|
if [ "$frames" -lt 1 ]; then
|
|
echo "FAIL: no HSMS frames dissected (capture may not include traffic)"
|
|
echo "--- tshark output (last 60 lines) ---"; tail -60 "$TSHARK_LOG"
|
|
exit 1
|
|
fi
|
|
|
|
# Spot-check for expected wire elements: each MUST appear at least once
|
|
# in a healthy demo run. Linktest is intentionally disabled in the
|
|
# demo client config (Timers::linktest = 0ms) so it's not expected.
|
|
# Tokens here are what the tshark -V protocol tree actually emits:
|
|
# "Header (Select.req)" for control frames
|
|
# "SType (Session type):" on every HSMS frame
|
|
# "Data message" for SECS data frames
|
|
# "Header (S" + SxxFyy form for SECS data frames
|
|
expected=("Header (Select.req)" "Header (Select.rsp)" "Header (Separate.req)"
|
|
"Data message" "SType (Session type):")
|
|
missing=()
|
|
for token in "${expected[@]}"; do
|
|
if ! grep -q -F "$token" "$TSHARK_LOG"; then
|
|
missing+=("$token")
|
|
fi
|
|
done
|
|
if [ ${#missing[@]} -gt 0 ]; then
|
|
echo "FAIL: expected tokens missing from tshark output:"
|
|
for m in "${missing[@]}"; do echo " - $m"; done
|
|
echo "--- tshark output (first 200 lines) ---"; head -200 "$TSHARK_LOG"
|
|
exit 1
|
|
fi
|
|
|
|
# Histogram of distinct S/F pairs seen (proves we exercise breadth, not
|
|
# just one message type).
|
|
distinct_sf=$(grep -oE 'Header \(S[0-9]+F[0-9]+\)' "$TSHARK_LOG" | sort -u | wc -l)
|
|
echo ""
|
|
echo "tshark dissection summary:"
|
|
echo " HSMS frames dissected: $frames"
|
|
echo " Select.req frames: $(grep -c -F 'Header (Select.req)' "$TSHARK_LOG" || echo 0)"
|
|
echo " Select.rsp frames: $(grep -c -F 'Header (Select.rsp)' "$TSHARK_LOG" || echo 0)"
|
|
echo " Separate.req frames: $(grep -c -F 'Header (Separate.req)' "$TSHARK_LOG" || echo 0)"
|
|
echo " SECS data frames: $(grep -c -F 'Data message' "$TSHARK_LOG" || echo 0)"
|
|
echo " distinct S/F pairs: $distinct_sf"
|
|
echo ""
|
|
echo "PASS: tshark dissected $frames HSMS frame(s) with no malformed-packet warnings"
|
|
|
|
rm -rf "$TMPDIR"
|