Files
secs-gem/interop/tshark_validate.sh
T
raphael b031f057af docs: customer-ready sweep + README restructure + tshark CI fix
Audit pass over the public-facing surface so a customer can read it
end-to-end without tripping on stale numbers or self-contradictions.

README + docs accuracy:
- Test counts 426 → 445, assertions 2 557 → 2 753 (verified via
  doctest run); E5 row was missing test_e5_kat (19 cases)
- Interop checks 24 → 31, COMPLIANCE.md message count 149 → 164,
  COMPLIANCE.md "291 cases / 1515 assertions" → 445 / 2 753
- README "60+ test IDs" for MES_INTEROP → actual 59
- PVD example counts: 32 SVIDs/17 CEIDs → 29/21, "~40 handlers
  in ~200 lines" → 51 in ~460, "~700 lines" → ~1,100; main.cpp
  header table-of-contents resynced with the actual 7 sections

Out-of-scope honesty (COMPLIANCE.md §8 + FAQ.md):
- Removed HSMS-GS (was both  implemented in §1 and "out of scope"
  in §8; INTEGRATION.md §7 documents using it)
- Removed multi-block SECS-I (split_message/assemble_message exist
  with 4 dedicated tests)
- Added serial-port wiring as the genuine open  item — FSM is
  tested end-to-end over TCP; only the asio serial_port glue is
  deferred
- COMPLIANCE.md intro now lists E42 and notes "E37 (SS + GS)"

README restructure:
- Moved the 8-command proof table and per-standard test-coverage
  table to a new PROOFS.md (72 lines)
- README now leads with what / Quick start / Documentation map,
  then a one-paragraph "How it's proved" linking to PROOFS.md
- Updated cross-refs in FAQ.md, GLOSSARY.md, VERIFICATION.md, and
  interop/README.md to point at PROOFS.md

CI fix — tshark-dissector job:
- interop/tshark_validate.sh hardcoded /app/build/secs_server etc.
  which only works inside the docker image.  Now derives ROOT from
  the script's own location and accepts BUILD/SERVER/CLIENT/DATA
  env overrides, so CI can run it from the workspace dir
- Verified still passes in docker (69 frames, 0 malformed)

.gitignore:
- Added build-fuzz/ and build-tsan/ (were showing as untracked)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 18:59:17 +02:00

144 lines
5.4 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), or on
# any host with tshark + tcpdump installed. Resolves $ROOT from the
# script's own location, so /app/... and any out-of-source checkout
# both work without env overrides.
#
# 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
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
PORT=${PORT:-5099}
BUILD=${BUILD:-$ROOT/build}
SERVER=${SERVER:-$BUILD/secs_server}
CLIENT=${CLIENT:-$BUILD/secs_client}
DATA=${DATA:-$ROOT/data}
[ -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 "$DATA/equipment.yaml" \
--state-table "$DATA/control_state.yaml" \
--pj-state-table "$DATA/process_job_state.yaml" \
--cj-state-table "$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"