Initial commit: simtrace2-pysniff v0.1.0

Python-based SIMtrace2 sniffer — drop-in replacement for simtrace2-sniff.
Single dependency (pyusb), auto-reconnect, GSMTAP/PCAP output, library API.

Modules:
  protocol.py — SIMtrace2 USB sniffing protocol parser
  device.py   — USB discovery + SniffSession with exponential backoff reconnect
  gsmtap.py   — GSMTAP UDP sender for Wireshark
  pcap.py     — PCAP file writer (LINKTYPE_GSMTAP)
  dump.py     — hex dump formatters + file output
  __main__.py — CLI entry point

Startup script sniff-start.sh handles udev permission checks and shows
install instructions. udev rule uses TAG+=uaccess (systemd-logind ACL).
This commit is contained in:
catarrh committed 2026-08-10 23:17:04 +03:00
commit 73b0277ef7
13 files changed
+1289

No files matched your search

+235
View File
@@ -0,0 +1,235 @@
"""Tests for protocol.py message parsing."""
import struct
import unittest
from simtrace2_pysniff.protocol import (
parse_message,
MSG_HDR_FMT,
MSG_HDR_SIZE,
MSG_HDR_SIZE,
flags_to_names,
CHANGE_FLAG_CARD_INSERT,
CHANGE_FLAG_TIMEOUT_WT,
DATA_FLAG_ERROR_CHECKSUM,
DATA_FLAG_ERROR_MALFORMED,
DATA_FLAG_ERROR_INCOMPLETE,
SIMTRACE_MSGC_SNIFF,
SIMTRACE_MSGT_SNIFF_CHANGE,
SIMTRACE_MSGT_SNIFF_FIDI,
SIMTRACE_MSGT_SNIFF_ATR,
SIMTRACE_MSGT_SNIFF_PPS,
SIMTRACE_MSGT_SNIFF_TPDU,
)
def _build_hdr(msg_class, msg_type, slot_nr=0, seq_nr=0, payload=b''):
msg_len = MSG_HDR_SIZE + len(payload)
return struct.pack(MSG_HDR_FMT, msg_class, msg_type, seq_nr, slot_nr, 0, msg_len) + payload
class TestParseMessage(unittest.TestCase):
def test_empty_buffer(self):
msg, consumed = parse_message(b'')
self.assertIsNone(msg)
self.assertEqual(consumed, 0)
def test_too_short_for_header(self):
msg, consumed = parse_message(b'\x00' * 5)
self.assertIsNone(msg)
self.assertEqual(consumed, 0)
def test_non_sniff_class_skipped(self):
payload = b'junk_data'
buf = _build_hdr(0, 0, payload=payload)
msg, consumed = parse_message(buf)
self.assertIsNone(msg)
self.assertEqual(consumed, len(buf))
def test_msg_len_smaller_than_header_skip(self):
buf = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_CHANGE)
buf = bytearray(buf)
struct.pack_into('<H', buf, 6, 4) # set msg_len to 4 (< MSG_HDR_SIZE)
msg, consumed = parse_message(bytes(buf))
self.assertIsNone(msg)
self.assertEqual(consumed, MSG_HDR_SIZE)
def test_incomplete_message_needs_more_data(self):
buf = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_ATR,
payload=b'\x01\x02\x03')
msg, consumed = parse_message(buf[:-2])
self.assertIsNone(msg)
self.assertEqual(consumed, 0)
def test_change_message_card_insert(self):
payload = struct.pack('<I', CHANGE_FLAG_CARD_INSERT)
buf = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_CHANGE,
slot_nr=1, seq_nr=5, payload=payload)
msg, consumed = parse_message(buf)
self.assertEqual(consumed, len(buf))
self.assertEqual(msg.type, 'change')
self.assertEqual(msg.type_hex, '0x00')
self.assertEqual(msg.slot_nr, 1)
self.assertEqual(msg.seq_nr, 5)
self.assertEqual(msg.flags, CHANGE_FLAG_CARD_INSERT)
self.assertEqual(msg.data, b'')
def test_change_message_timeout(self):
payload = struct.pack('<I', CHANGE_FLAG_TIMEOUT_WT)
buf = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_CHANGE,
payload=payload)
msg, consumed = parse_message(buf)
self.assertEqual(msg.type, 'change')
self.assertEqual(msg.flags, CHANGE_FLAG_TIMEOUT_WT)
def test_fidi_message(self):
payload = struct.pack('<I', 0x97) # Fi=9, Di=7
buf = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_FIDI,
payload=payload)
msg, consumed = parse_message(buf)
self.assertEqual(msg.type, 'fidi')
self.assertEqual(len(msg.data), 0)
self.assertEqual(msg.flags, 0x97)
def test_atr_message(self):
atr = b'\x3b\x9e\x12\x34\x56\x78\x90'
payload = struct.pack('<IH', 0, len(atr)) + atr
buf = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_ATR,
payload=payload)
msg, consumed = parse_message(buf)
self.assertEqual(consumed, len(buf))
self.assertEqual(msg.type, 'atr')
self.assertEqual(msg.data, atr)
self.assertEqual(msg.flags, 0)
def test_atr_message_with_flags(self):
atr = b'\x3b\x00'
flags = DATA_FLAG_ERROR_CHECKSUM | DATA_FLAG_ERROR_MALFORMED
payload = struct.pack('<IH', flags, len(atr)) + atr
buf = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_ATR,
payload=payload)
msg, _ = parse_message(buf)
self.assertEqual(msg.flags, flags)
def test_pps_message(self):
pps = b'\xff\x10\x97\x7e'
payload = struct.pack('<IH', 0, len(pps)) + pps
buf = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_PPS,
payload=payload)
msg, consumed = parse_message(buf)
self.assertEqual(msg.type, 'pps')
self.assertEqual(msg.data, pps)
def test_tpdu_message(self):
tpdu = b'\xa0\xa4\x00\x00\x02\x3f\x00'
payload = struct.pack('<IH', 0, len(tpdu)) + tpdu
buf = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_TPDU,
payload=payload)
msg, _ = parse_message(buf)
self.assertEqual(msg.type, 'tpdu')
self.assertEqual(msg.data, tpdu)
def test_tpdu_flags_incomplete(self):
tpdu = b'\x00\xb0\x00\x00\x0a'
flags = DATA_FLAG_ERROR_INCOMPLETE
payload = struct.pack('<IH', flags, len(tpdu)) + tpdu
buf = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_TPDU,
payload=payload)
msg, _ = parse_message(buf)
self.assertEqual(msg.flags, flags)
def test_timestamp_passed_through(self):
buf = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_CHANGE,
payload=struct.pack('<I', 0))
msg, _ = parse_message(buf, timestamp=12345.678)
self.assertEqual(msg.timestamp, 12345.678)
def test_multiple_messages_in_buffer(self):
buf1 = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_CHANGE,
payload=struct.pack('<I', 1))
atr_payload = struct.pack('<IH', 0, 2) + b'\x3b\x00'
buf2 = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_ATR,
payload=atr_payload)
combined = buf1 + buf2
msg1, consumed1 = parse_message(combined)
self.assertIsNotNone(msg1)
self.assertEqual(msg1.type, 'change')
self.assertEqual(consumed1, len(buf1))
remaining = combined[consumed1:]
msg2, consumed2 = parse_message(remaining)
self.assertIsNotNone(msg2)
self.assertEqual(msg2.type, 'atr')
self.assertEqual(consumed2, len(buf2))
def test_trailing_garbage_before_valid_message(self):
msg1 = _build_hdr(0, 1, slot_nr=3, payload=b'card_emu_data!!!')
buf = msg1 + _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_CHANGE,
payload=struct.pack('<I', 1))
msg, consumed = parse_message(buf)
self.assertIsNone(msg)
self.assertEqual(consumed, len(msg1))
msg, consumed = parse_message(buf[len(msg1):])
self.assertIsNotNone(msg)
self.assertEqual(msg.type, 'change')
def test_non_sniff_msg_class_with_short_msg_len(self):
hdr = struct.pack(MSG_HDR_FMT, 0, 0, 0, 0, 0, 4)
msg, consumed = parse_message(hdr + b'\x00')
self.assertIsNone(msg)
self.assertEqual(consumed, MSG_HDR_SIZE)
def test_non_sniff_msg_class_known_len_skip(self):
payload = b'test_data_for_cardem'
buf = _build_hdr(1, 0, payload=payload)
msg, consumed = parse_message(buf)
self.assertIsNone(msg)
self.assertEqual(consumed, len(buf))
def test_zero_length_data(self):
payload = struct.pack('<IH', 0, 0)
buf = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_ATR,
payload=payload)
msg, consumed = parse_message(buf)
self.assertEqual(msg.type, 'atr')
self.assertEqual(msg.data, b'')
self.assertEqual(msg.flags, 0)
def test_truncated_data_hdr(self):
buf = _build_hdr(SIMTRACE_MSGC_SNIFF, SIMTRACE_MSGT_SNIFF_TPDU,
payload=b'\x00\x00\x00\x00\x10')
msg, consumed = parse_message(buf)
self.assertIsNone(msg)
self.assertEqual(consumed, len(buf))
class TestFlagsToNames(unittest.TestCase):
def test_empty(self):
self.assertEqual(flags_to_names(0, {}), [])
def test_single(self):
names = flags_to_names(CHANGE_FLAG_CARD_INSERT, {
CHANGE_FLAG_CARD_INSERT: 'card inserted',
})
self.assertEqual(names, ['card inserted'])
def test_multiple(self):
names = flags_to_names(
CHANGE_FLAG_CARD_INSERT | CHANGE_FLAG_TIMEOUT_WT,
{
CHANGE_FLAG_CARD_INSERT: 'card inserted',
CHANGE_FLAG_TIMEOUT_WT: 'timeout',
}
)
self.assertEqual(names, ['card inserted', 'timeout'])
def test_unknown_bit_preserved(self):
names = flags_to_names(0xDEAD0000, {})
self.assertIn('unknown(0xdead0000)', names)
if __name__ == '__main__':
unittest.main()