Extended logical channels: CLA 0-19, ATR card capabilities, MANAGE CHANNEL (v2.6.0)

- CLA decode (TS 102 221 §10.1.1/§10.3): standard channels 0-3 from b2-b1
  and the extended channels 4-19 from b4-b1.  The old further-format rule
  read only b2-b1 (channel 8 read as 6, 19 as 7) and the ETSI '11x0 xxxx'
  coding (0xC0-0xEF) was not handled at all; the class ranges now follow the
  specs (0x80-0x8F UICC, 0xA0-0xAF SIM, 0xC0-0xEF ETSI extended, 0x20-0x3F
  reserved, 0xFF invalid) and SM/chain are decoded per family — the SM
  extraction and the chaining bit (b5, not b6) were off by one bit
- per-channel state: database._decode_rows/_replay_context use the shared
  cla_channel() helper — before, CLA & 0x03 with no +4, so a channel-5
  SELECT clobbered channel 1's selection and a channel-8 one channel 0's
  (the local traces already open channels: 29 MANAGE CHANNEL opens, 1-2)
- COMPRION T=0 import: the synthesized GET RESPONSE keeps the channel —
  a plain CLA for 0-3 (as real terminals do) and 01x0 for 4-19
- ATR historical bytes: the UICC's COMPACT-TLV objects (TS 102 221 §6.3.1)
  with card service data, the card capabilities (ISO 7816-4 §8.1.1.2.7
  Table 88: assignment methods and max logical channels — the workspace
  cards announce 4 and "8 or more") and the status indicator ('00' trailing
  bytes / the '8Y' TLV); a truncated ATR no longer raises
- MANAGE CHANNEL success replies decode the assigned channel byte
- capture/import: a 'tpdu' packet that is not APDU-shaped (shorter than the
  header, CLA FF, ISO-reserved class) is skipped and counted, so a
  character-level or mis-sampled stream no longer pollutes the decode
  (the local DB's outlier sessions are exactly that)
- tests: CLA table vectors, per-channel state isolation on channels 8/12,
  COMPRION GR channels, filetrack extended channels, ATR compact-TLV and
  card-capability cases, the capture skip counter, the PWA rows; 527 Python
  + 47 Node tests
- README/README_RUS document the extended channels, card capabilities and
  the skip counter; PWA_VERSION/pyproject/version.py 2.6.0, SW v72
This commit is contained in:
catarrh committed 2026-10-17 22:42:11 +03:00
1 parent cbeeed3608
commit 64433f9739
18 files changed
+683 -88

No files matched your search

+19 -2
View File
@@ -10,6 +10,8 @@ from simtrace2_pysniff.gsmtap import (GsmtapReceiver, GSMTAP_SIM_ATR,
GSMTAP_SIM_PPS_REQ, GSMTAP_SIM_PPS_RSP,
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT)
from .decode import looks_like_apdu
def _ts():
"""Local-time stamp for server-side capture log lines."""
@@ -120,6 +122,7 @@ class CaptureManager:
self._latest_msg_id = 0
self._msg_count = 0
self._byte_count = 0
self._skipped = 0
self._log_interval = log_interval
@property
@@ -138,6 +141,11 @@ class CaptureManager:
def latest_msg_id(self):
return self._latest_msg_id
@property
def skipped(self):
"""Packets dropped because they were not APDU-shaped."""
return self._skipped
@property
def device_connected(self):
"""True while the capture device is connected, or None when the
@@ -156,6 +164,7 @@ class CaptureManager:
self._latest_msg_id = 0
self._msg_count = 0
self._byte_count = 0
self._skipped = 0
self._backend.start()
self._thread = threading.Thread(target=self._capture_loop, daemon=True)
@@ -187,7 +196,7 @@ class CaptureManager:
return None
self._db.close_session(sid)
_log(f'Capture stopped: session={sid} messages={self._msg_count} '
f'bytes={self._byte_count}')
f'bytes={self._byte_count} skipped={self._skipped}')
return sid
def _capture_loop(self):
@@ -195,6 +204,14 @@ class CaptureManager:
for msg_type, data, flags in self._backend.iter_messages():
if self._session_id is None:
break
if msg_type == 'tpdu' and not looks_like_apdu(data):
# Not APDU-shaped at all: a mis-sampled or character-level
# GSMTAP stream would only pollute the decode.
self._skipped += 1
if self._skipped <= 1 or self._skipped % 100 == 0:
_log(f'Capture: skipped packet {self._skipped} that is '
f'not APDU-shaped — check the sniffer stream')
continue
try:
elapsed = round(time.monotonic() - self._start_time, 3)
mid = self._db.insert_message(
@@ -226,4 +243,4 @@ class CaptureManager:
dropped = getattr(self._backend, 'dropped', 0)
_log(f'Capture alive: session={self._session_id} '
f'messages={self._msg_count} bytes={self._byte_count} '
f'dropped={dropped}')
f'dropped={dropped} skipped={self._skipped}')