Files
secs-gem/interop/tshark_validate.sh
T
raphael 5baf3f4dc7 verify: tshark HSMS dissector validation (independent third codec)
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>
2026-06-09 16:02:38 +02:00

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"