feat: GSMTAP-SIM APDU streaming for Wireshark / SIMtrace Analyser (v3.1.0)

Streams every APDU the server sends or receives as GSMTAP-SIM UDP packets,
so a live capture can be followed in Wireshark or the SIMtrace Analyser
without a hardware sniffer.  CLI-only: --gsmtap [HOST[:PORT]], default
target 127.0.0.1:4729; no UI or API.

- pysim_simple_server/gsmtap.py: 16-byte big-endian GSMTAP-SIM header
  (type 0x04, sub_type 0x00 = APDU / 0x01 = ATR) + raw APDU bytes, a
  fire-and-forget non-blocking sender that never raises into card I/O, an
  ApduTracer (a response is sent as data + SW1SW2, the wire form) and a
  fan-out tracer.  The packet layout is byte-identical to
  sigrok-iso7816-stream / simtrace2-sniff (verified against the sigrok
  module) and is what the analyser's GSMTAP receiver expects.
- __main__.py: --gsmtap option, tracer installed on the shared transport
  before the first APDU, combined with --apdu-trace via the fan-out and
  re-attached across equips (pySim nulls the tracer on every equip); one
  ATR packet per equip from _apply_equipped_card/_send_gsmtap_atr.
- start.sh/start.bat: forward their extra arguments to the server, so
  ./start.sh --gsmtap works.
- tests: packet layout, loopback UDP delivery, tracer mapping, target
  parsing, fan-out; docs (READMEs, help EN/RU, AGENTS); version 3.1.0,
  sw.js simple-v235.
This commit is contained in:
2026-09-22 01:41:14 +03:00
parent 910c5abfad
commit a387cdc1b6
13 changed files with 352 additions and 26 deletions
+2 -1
View File
@@ -662,7 +662,7 @@ The bundled Python server wraps [pySim](https://osmocom.org/projects/pysim/wiki)
| `setup.sh` / `setup.bat` | Creates `.venv/`, installs pysim and the server. Run once after cloning. |
| `start.sh` / `start.bat` | Starts the server from the venv (serves the PWA + API on `:8080`). |
`start.sh` auto-detects the reader (PC/SC if `pcscd` is running, else `/dev/ttyUSB0`); `start.bat` always uses `-p 0` (PC/SC is built into Windows). If no reader is found the server still starts ("Reader: none") — initialize the card later via the **Equip** button.
`start.sh` auto-detects the reader (PC/SC if `pcscd` is running, else `/dev/ttyUSB0`); `start.bat` always uses `-p 0` (PC/SC is built into Windows). Extra arguments are passed through to the server, e.g. `./start.sh --gsmtap` or `start.bat --gsmtap 10.0.0.5:4729`. If no reader is found the server still starts ("Reader: none") — initialize the card later via the **Equip** button.
### Manual installation
@@ -685,6 +685,7 @@ pysim-simple-server --http-port 8080
| `-d` / `--device` | Serial device path |
| `--no-card-init` | Skip card init to preserve the CAT session (no file manager) |
| `--apdu-trace` | Log APDU-level traces to stderr |
| `--gsmtap [HOST[:PORT]]` | Stream every APDU (and the card ATR at equip) as GSMTAP-SIM UDP packets for Wireshark / SIMtrace Analyser (`--capture gsmtap`); default target `127.0.0.1:4729`. Combines with `--apdu-trace` |
| `--log-requests` | Log request/response payloads to stderr |
| `--sms-oa` / `--sms-sm-sc` | SMS-DELIVER originating address / SM-SC for PoR-in-submit |
| `--terminal-profile` | TERMINAL PROFILE payload hex (default: 33-byte real-handset profile that advertises BIP events/commands; the live card ignores HTTP OTA without it) |
+2 -1
View File
@@ -645,7 +645,7 @@ PWA проверяет версию сервера при подключении
| `setup.sh` / `setup.bat` | Создаёт `.venv/`, ставит pysim и сервер. Запускать один раз после клонирования. |
| `start.sh` / `start.bat` | Запускает сервер из venv (раздаёт PWA + API на `:8080`). |
`start.sh` автоопределяет ридер (PC/SC при работающем `pcscd`, иначе `/dev/ttyUSB0`); `start.bat` всегда использует `-p 0`. Без ридера сервер всё равно стартует («Reader: none») — карту можно инициализировать позже кнопкой **Equip**.
`start.sh` автоопределяет ридер (PC/SC при работающем `pcscd`, иначе `/dev/ttyUSB0`); `start.bat` всегда использует `-p 0`. Дополнительные аргументы передаются серверу, например `./start.sh --gsmtap` или `start.bat --gsmtap 10.0.0.5:4729`. Без ридера сервер всё равно стартует («Reader: none») — карту можно инициализировать позже кнопкой **Equip**.
### Ручная установка
@@ -668,6 +668,7 @@ pysim-simple-server --http-port 8080
| `-d` / `--device` | Путь к serial-устройству |
| `--no-card-init` | Пропустить инициализацию карты (сохранить CAT-сессию) |
| `--apdu-trace` | Лог APDU-трафика в stderr |
| `--gsmtap [HOST[:PORT]]` | Поток всех APDU (и ATR карты при equip) в виде GSMTAP-SIM UDP-пакетов для Wireshark / SIMtrace Analyser (`--capture gsmtap`); цель по умолчанию `127.0.0.1:4729`. Совместимо с `--apdu-trace` |
| `--log-requests` | Лог запросов/ответов в stderr |
| `--sms-oa` / `--sms-sm-sc` | Адрес отправителя SMS-DELIVER / SM-SC для PoR-in-submit |
| `--terminal-profile` | Hex TERMINAL PROFILE (по умолчанию — 33-байтовый профиль реального телефона с BIP-событиями/командами; без него живая карта не запускает HTTP OTA) |
+1 -1
View File
@@ -546,7 +546,7 @@ start.bat # запускает сервер (PWA + API)</pre>
<li><strong>Резерв сервера</strong> — при запуске без аргументов ридера сервер сам опрашивает PC/SC-ридер при старте (3 попытки с интервалом 2&nbsp;с)</li>
<li><strong>Serial-ридеры</strong> — запустите сервер вручную с <code class="font-mono text-sm">-d /dev/ttyUSB0</code> (Linux)</li>
</ul>
<p class="text-sm mb-3">Если карта отсутствует, вкладка &laquo;Картридер&raquo; показывает &laquo;Карта не обнаружена. Вставьте карту и нажмите Подключить карту&raquo;. Вставьте карту — сервер инициализирует её автоматически (auto-equip включён по умолчанию) — либо нажмите <strong>Equip card</strong>. Сам сервер запускается и с пустым картридером: отсутствие карты — нормальное состояние, а не ошибка.</p>
<p class="text-sm mb-3">Если карта отсутствует, вкладка &laquo;Картридер&raquo; показывает &laquo;Карта не обнаружена. Вставьте карту и нажмите Подключить карту&raquo;. Вставьте карту — сервер инициализирует её автоматически (auto-equip включён по умолчанию) — либо нажмите <strong>Equip card</strong>. Сам сервер запускается и с пустым картридером: отсутствие карты — нормальное состояние, а не ошибка. Дополнительные аргументы <code class="font-mono text-sm">start.sh</code>/<code class="font-mono text-sm">start.bat</code> передаются серверу (например, <code class="font-mono text-sm">./start.sh --gsmtap</code> — поток APDU в виде GSMTAP-SIM пакетов для Wireshark / SIMtrace Analyser).</p>
<h3 id="manual-install" class="text-lg font-medium mb-2">9.6 Ручная установка</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3"># Создать и активировать venv
+1 -1
View File
@@ -546,7 +546,7 @@ start.bat # starts the server (serves PWA + API)</pre>
<li><strong>Server fallback</strong> — started without reader arguments, the server itself probes for a PC/SC reader at startup (3 attempts, 2&nbsp;s apart)</li>
<li><strong>Serial readers</strong> — start the server manually with <code class="font-mono text-sm">-d /dev/ttyUSB0</code> (Linux)</li>
</ul>
<p class="text-sm mb-3">If no card is present, the Card reader tab shows &ldquo;No card detected&rdquo;. Insert the card — the server auto-equips it (auto-equip is on by default) — or click <strong>Equip card</strong>. The server itself starts fine with an empty reader; the absence of a card is a normal state, not an error.</p>
<p class="text-sm mb-3">If no card is present, the Card reader tab shows &ldquo;No card detected&rdquo;. Insert the card — the server auto-equips it (auto-equip is on by default) — or click <strong>Equip card</strong>. The server itself starts fine with an empty reader; the absence of a card is a normal state, not an error. Extra arguments given to <code class="font-mono text-sm">start.sh</code>/<code class="font-mono text-sm">start.bat</code> are passed through to the server (e.g. <code class="font-mono text-sm">./start.sh --gsmtap</code> to stream APDUs as GSMTAP-SIM packets for Wireshark / SIMtrace Analyser).</p>
<h3 id="manual-install" class="text-lg font-medium mb-2">9.6 Manual installation</h3>
<pre class="font-mono text-xs bg-gray-100 dark:bg-slate-800 rounded p-3 mb-3"># Create and activate a venv
+1 -1
View File
@@ -1436,7 +1436,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.0.1';
const SIMPLE_VERSION = '3.1.0';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v234';
const CACHE = 'simple-v235';
const URLS = [
'index.html',
'help.html',
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.0.1"
version = "3.1.0"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+35 -7
View File
@@ -13,7 +13,8 @@ from pySim.cards import UiccCardBase
from .shell import load_pysim_app
from . import fastinit
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog, _set_menu_timeout, start_card_monitor, set_auto_equip, _read_iccid, _netstate_read, _netstate_install, _LineFilter
from . import gsmtap
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog, _set_menu_timeout, start_card_monitor, set_auto_equip, _read_iccid, _netstate_read, _netstate_install, _send_gsmtap_atr, _LineFilter
_server_start = 0
@@ -70,6 +71,10 @@ def main():
help='Auto-send a timeout TERMINAL RESPONSE if a paused STK command is not answered (default: 60, 0 disables)')
parser.add_argument('--no-auto-equip', action='store_true', default=False,
help='Do not automatically initialize a card right after it is inserted (default: auto-equip on)')
parser.add_argument('--gsmtap', nargs='?', const=gsmtap.DEFAULT_TARGET, default=None,
metavar='HOST[:PORT]',
help='Stream every APDU (and the card ATR) as GSMTAP-SIM UDP packets for '
'Wireshark / SIMtrace Analyser (default target: %s)' % gsmtap.DEFAULT_TARGET)
opts = parser.parse_args()
opts.skip_card_init = opts.no_card_init
@@ -79,6 +84,25 @@ def main():
if opts.menu_timeout is not None:
_set_menu_timeout(opts.menu_timeout)
set_auto_equip(not opts.no_auto_equip and not opts.skip_card_init)
# APDU tracers: --apdu-trace (stderr) and --gsmtap (GSMTAP-SIM UDP) can be
# combined; the fan-out keeps pySim's single-tracer interface.
gsmtap_sender = None
tracers = []
if opts.apdu_trace:
tracers.append(_LoggingApduTracer())
if opts.gsmtap is not None:
try:
gsmtap_host, gsmtap_port = gsmtap.parse_target(opts.gsmtap)
gsmtap_sender = gsmtap.GsmtapSender(gsmtap_host, gsmtap_port)
tracers.append(gsmtap.GsmtapApduTracer(gsmtap_sender))
sys.stderr.write('GSMTAP: streaming APDUs to %s\n' % gsmtap_sender.target)
except (ValueError, OSError) as e:
sys.stderr.write('GSMTAP: disabled (%s)\n' % e)
tracer = None
if len(tracers) == 1:
tracer = tracers[0]
elif tracers:
tracer = gsmtap.FanoutApduTracer(tracers)
sl = None
scc = None
card = None
@@ -104,8 +128,8 @@ def main():
try:
kwargs = {}
if opts.apdu_trace:
kwargs['apdu_tracer'] = _LoggingApduTracer()
if tracer is not None:
kwargs['apdu_tracer'] = tracer
t_phase = time.time()
sl = mod.init_reader(opts, **kwargs)
_tlog('init_reader: %.0fms' % ((time.time() - t_phase) * 1000))
@@ -203,7 +227,8 @@ def main():
_tlog('terminal_profile_drain: %.0fms' % ((time.time() - t_phase) * 1000))
except Exception:
traceback.print_exc(file=sys.stderr)
if app is not None and opts.apdu_trace:
if app is not None and tracer is not None:
if opts.apdu_trace:
# PysimApp.__init__ routes PySimLogger through app.poutput() (app.stdout)
# and drops the root level to INFO. Re-route pysim's own APDU trace logging
# directly to fd 1 so it survives the app.stdout/StringIO redirection in the
@@ -211,9 +236,8 @@ def main():
PySimLogger.setup(print_callback=_log_stdout)
PySimLogger.set_level(logging.DEBUG)
# PysimApp.__init__ and every `equip` wipe the transport apdu_tracer
# (_onchange_apdu_trace sets it to None). Re-attach our tracer and make
# sure it stays attached across equip/re-equip.
tracer = _LoggingApduTracer()
# (_onchange_apdu_trace sets it to None). Re-attach our tracer(s) and make
# sure they stay attached across equip/re-equip.
def _reattach_tracer():
if app.card:
app.card._scc._tp.apdu_tracer = tracer
@@ -243,6 +267,10 @@ def main():
server.card_present = card is not None
server.card_session = 1 if card is not None else 0
server.iccid = iccid
server.gsmtap = gsmtap_sender
# Stream the ATR so a GSMTAP receiver (SIMtrace Analyser, Wireshark) has
# the session context before the first APDU of this session.
_send_gsmtap_atr(server)
# Network state monitor: install the state read during the startup init
# (right after the ICCID, before the TERMINAL PROFILE). No readable
# ICCID means the card is considered unusable - give up.
+151
View File
@@ -0,0 +1,151 @@
# coding=utf-8
"""GSMTAP-SIM UDP sender for live APDU capture.
Streams every APDU the server sends/receives as GSMTAP-SIM packets, so a
GSMTAP receiver (SIMtrace Analyser ``--capture gsmtap``, Wireshark, or
simtrace2-sniff) can follow the card dialogue live. Enabled with
``--gsmtap [HOST[:PORT]]`` only (default ``127.0.0.1:4729``) - there is no
UI or API for it.
The packet format is the one shared by libosmocore's ``gsmtap.h``,
simtrace2-sniff, sigrok-iso7816-stream and the SIMtrace Analyser: a 16-byte
big-endian header (version 2, ``hdr_len`` 4, type 0x04 = SIM, sub_type,
``res`` flags) followed by the raw APDU/TPDU bytes. A response is sent as
``data + SW1SW2`` (the wire form); the receiver infers the direction from
the ISO 7816 case, exactly like a sniffer capture.
Sending is fire-and-forget on a non-blocking socket: a missing listener must
never affect card I/O.
"""
import socket
import struct
from pySim.transport import ApduTracer
GSMTAP_VERSION = 0x02
GSMTAP_HDR_LEN = 4 # in 32-bit words (16 bytes)
GSMTAP_TYPE_SIM = 0x04
GSMTAP_SIM_APDU = 0x00
GSMTAP_SIM_ATR = 0x01
GSMTAP_UDP_PORT = 4729
DEFAULT_TARGET = '127.0.0.1:%d' % GSMTAP_UDP_PORT
_HDR_FMT = '!BBBBHBBIBBBB' # 16 bytes, big-endian
_HDR_SIZE = struct.calcsize(_HDR_FMT)
def build_packet(sub_type, data, flags=0, slot_nr=0):
"""Build a complete GSMTAP-SIM packet (header + payload) as bytes."""
hdr = struct.pack(
_HDR_FMT,
GSMTAP_VERSION, # version
GSMTAP_HDR_LEN, # hdr_len (in 32-bit words)
GSMTAP_TYPE_SIM, # type
0, # timeslot
0, # arfcn
0, # signal_dbm
0, # snr_db
0, # frame_number
sub_type, # sub_type
0, # antenna_nr
slot_nr, # sub_slot
flags, # res (GSMTAP_FLAG_*; 0 here)
)
return hdr + bytes(data)
def parse_target(target):
"""'HOST[:PORT]' -> (host, port); empty/None -> the default target."""
text = str(target or '').strip()
if not text:
text = DEFAULT_TARGET
if ':' in text:
host, _sep, port = text.rpartition(':')
return host or '127.0.0.1', int(port)
return text, GSMTAP_UDP_PORT
class GsmtapSender:
"""Fire-and-forget GSMTAP-SIM UDP sender (never raises on send)."""
def __init__(self, host='127.0.0.1', port=GSMTAP_UDP_PORT):
self._addr = (host, int(port))
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# A full socket buffer must never stall a card operation.
self._sock.setblocking(False)
@property
def target(self):
return '%s:%d' % self._addr
def send(self, sub_type, data, flags=0, slot_nr=0):
try:
self._sock.sendto(build_packet(sub_type, data, flags, slot_nr),
self._addr)
except OSError:
pass
def send_apdu(self, data, slot_nr=0):
self.send(GSMTAP_SIM_APDU, data, slot_nr=slot_nr)
def send_atr(self, data, slot_nr=0):
self.send(GSMTAP_SIM_ATR, data, slot_nr=slot_nr)
def close(self):
try:
self._sock.close()
except OSError:
pass
class GsmtapApduTracer(ApduTracer):
"""pySim APDU tracer that streams every APDU as a GSMTAP-SIM packet.
Commands are sent as-is; a response is sent as ``data + SW1SW2`` so the
receiver sees the same wire TPDU a hardware sniffer would capture.
Malformed hex never raises into pySim's transport.
"""
def __init__(self, sender):
super().__init__()
self.sender = sender
def trace_command(self, cmd):
if not cmd:
return
try:
self.sender.send_apdu(bytes.fromhex(cmd))
except (ValueError, TypeError):
pass
def trace_response(self, cmd, sw, resp):
data = (resp or '') + (sw or '')
if not data:
return
try:
self.sender.send_apdu(bytes.fromhex(data))
except (ValueError, TypeError):
pass
class FanoutApduTracer(ApduTracer):
"""Forward tracer callbacks to several tracers (e.g. stderr + GSMTAP)."""
def __init__(self, tracers):
super().__init__()
self.tracers = list(tracers)
def trace_command(self, cmd):
for tracer in self.tracers:
tracer.trace_command(cmd)
def trace_response(self, cmd, sw, resp):
for tracer in self.tracers:
tracer.trace_response(cmd, sw, resp)
def trace_reset(self):
for tracer in self.tracers:
tracer.trace_reset()
+25 -1
View File
@@ -28,7 +28,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.0.1'
VERSION = '3.1.0'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -2532,10 +2532,34 @@ def _handle_card_disconnect():
_reset_proactive_log()
def _send_gsmtap_atr(server):
"""Stream the card's ATR as a GSMTAP-SIM packet (--gsmtap only).
Sent at every equip so a GSMTAP receiver (SIMtrace Analyser, Wireshark)
has the session context before the first APDU of the new session."""
sender = getattr(server, 'gsmtap', None)
if sender is None:
return
rs = getattr(server, 'rs', None)
atr = None
if rs is not None:
try:
atr = (rs.identity or {}).get('ATR')
except Exception:
atr = None
if not atr:
return
try:
sender.send_atr(bytes.fromhex(atr))
except (ValueError, TypeError):
pass
def _apply_equipped_card(server):
"""Common post-equip state refresh + TERMINAL PROFILE, shared by the
/api/command equip branch and the auto-equip worker."""
global _CARD_CONNECTED
_send_gsmtap_atr(server)
server.stk_pending = None
server.menu_active = False
_cancel_menu_timeout()
+3 -3
View File
@@ -8,14 +8,14 @@ set VENV_DIR=%~dp0.venv
if exist "%VENV_DIR%\Scripts\pysim-simple-server.exe" (
echo Starting pysim-simple-server from venv on http://127.0.0.1:8080
echo Press Ctrl+C to stop.
"%VENV_DIR%\Scripts\pysim-simple-server.exe" --http-port 8080 -p 0
"%VENV_DIR%\Scripts\pysim-simple-server.exe" --http-port 8080 -p 0 %*
goto :eof
)
if exist "%~dp0pysim_simple_server\__main__.py" (
echo Starting pysim-simple-server from source on http://127.0.0.1:8080
echo Press Ctrl+C to stop.
python -m pysim_simple_server --http-port 8080 -p 0
python -m pysim_simple_server --http-port 8080 -p 0 %*
goto :eof
)
@@ -23,7 +23,7 @@ where pysim-simple-server >nul 2>&1
if %errorlevel% equ 0 (
echo Starting pysim-simple-server on http://127.0.0.1:8080
echo Press Ctrl+C to stop.
pysim-simple-server --http-port 8080 -p 0
pysim-simple-server --http-port 8080 -p 0 %*
goto :eof
)
+3 -2
View File
@@ -27,7 +27,7 @@ elif command -v pysim-simple-server &> /dev/null; then
SERVER="pysim-simple-server"
elif [ -f "$SCRIPT_DIR/pysim_simple_server/__main__.py" ]; then
echo "Starting pysim-simple-server from source on http://127.0.0.1:8080"
cd "$SCRIPT_DIR" && python3 -m pysim_simple_server --http-port 8080 $READER_ARGS
cd "$SCRIPT_DIR" && python3 -m pysim_simple_server --http-port 8080 $READER_ARGS "$@"
exit $?
else
echo "Error: pysim-simple-server not installed."
@@ -38,4 +38,5 @@ fi
echo "Starting pysim-simple-server on http://127.0.0.1:8080"
echo "Press Ctrl+C to stop."
$SERVER --http-port 8080 $READER_ARGS
echo "Extra arguments are passed to the server (e.g. ./start.sh --gsmtap)."
$SERVER --http-port 8080 $READER_ARGS "$@"
+120
View File
@@ -0,0 +1,120 @@
# coding=utf-8
"""Tests for the GSMTAP-SIM APDU sender (gsmtap.py)."""
import socket
import struct
import unittest
from pysim_simple_server import gsmtap
class PacketTests(unittest.TestCase):
def test_header_and_payload(self):
apdu = bytes.fromhex('00A40004023F00')
pkt = gsmtap.build_packet(gsmtap.GSMTAP_SIM_APDU, apdu)
self.assertEqual(len(pkt), 16 + len(apdu))
(version, hdr_len, pkt_type, timeslot, arfcn, signal, snr, frame,
sub_type, antenna, slot, res) = struct.unpack('!BBBBHBBIBBBB', pkt[:16])
self.assertEqual(version, gsmtap.GSMTAP_VERSION)
self.assertEqual(hdr_len, 4)
self.assertEqual(pkt_type, 0x04) # GSMTAP_TYPE_SIM
self.assertEqual((timeslot, arfcn, signal, snr, frame), (0, 0, 0, 0, 0))
self.assertEqual(sub_type, gsmtap.GSMTAP_SIM_APDU)
self.assertEqual((antenna, slot, res), (0, 0, 0))
self.assertEqual(pkt[16:], apdu)
def test_atr_subtype_and_slot(self):
pkt = gsmtap.build_packet(gsmtap.GSMTAP_SIM_ATR, b'\x3b\x00', slot_nr=2)
self.assertEqual(pkt[12], gsmtap.GSMTAP_SIM_ATR) # sub_type
self.assertEqual(pkt[14], 2) # sub_slot
self.assertEqual(pkt[16:], b'\x3b\x00')
def test_parse_target_defaults_and_overrides(self):
self.assertEqual(gsmtap.parse_target(None), ('127.0.0.1', 4729))
self.assertEqual(gsmtap.parse_target(''), ('127.0.0.1', 4729))
self.assertEqual(gsmtap.parse_target('127.0.0.1:4729'), ('127.0.0.1', 4729))
self.assertEqual(gsmtap.parse_target('10.0.0.5'), ('10.0.0.5', 4729))
self.assertEqual(gsmtap.parse_target('10.0.0.5:5000'), ('10.0.0.5', 5000))
self.assertEqual(gsmtap.parse_target(':5000'), ('127.0.0.1', 5000))
class SenderTests(unittest.TestCase):
def test_send_apdu_and_atr_reach_a_udp_listener(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('127.0.0.1', 0))
sock.settimeout(2.0)
host, port = sock.getsockname()
sender = gsmtap.GsmtapSender(host, port)
try:
sender.send_apdu(bytes.fromhex('00A40004023F00'))
sender.send_atr(bytes.fromhex('3B00'))
first, _addr = sock.recvfrom(2048)
second, _addr = sock.recvfrom(2048)
finally:
sender.close()
sock.close()
self.assertEqual(first[12], gsmtap.GSMTAP_SIM_APDU) # sub_type
self.assertEqual(first[16:], bytes.fromhex('00A40004023F00'))
self.assertEqual(second[12], gsmtap.GSMTAP_SIM_ATR)
self.assertEqual(second[16:], bytes.fromhex('3B00'))
def test_send_never_raises_without_a_listener(self):
sender = gsmtap.GsmtapSender('127.0.0.1', 1) # nothing listening
sender.send_apdu(b'\x00\xa4') # must not raise
sender.close()
class Recorder:
"""Stand-in for GsmtapSender recording the APDU payloads."""
def __init__(self):
self.apdus = []
def send_apdu(self, data, slot_nr=0):
self.apdus.append(bytes(data))
class TracerTests(unittest.TestCase):
def test_command_and_response_are_wire_shaped(self):
rec = Recorder()
tracer = gsmtap.GsmtapApduTracer(rec)
tracer.trace_command('00A40004023F00')
tracer.trace_response('00A40004023F00', '9000', '622982027821')
self.assertEqual(rec.apdus, [
bytes.fromhex('00A40004023F00'),
bytes.fromhex('6229820278219000'), # data + SW1SW2
])
def test_response_without_data_is_the_sw(self):
rec = Recorder()
gsmtap.GsmtapApduTracer(rec).trace_response('00B000000A', '6A82', '')
self.assertEqual(rec.apdus, [bytes.fromhex('6A82')])
def test_malformed_hex_never_raises(self):
rec = Recorder()
tracer = gsmtap.GsmtapApduTracer(rec)
tracer.trace_command('not-hex')
tracer.trace_response('00A4', None, None)
self.assertEqual(rec.apdus, [])
def test_fanout_forwards_every_callback(self):
class T:
def __init__(self):
self.calls = []
def trace_command(self, cmd):
self.calls.append(('cmd', cmd))
def trace_response(self, cmd, sw, resp):
self.calls.append(('rsp', cmd, sw, resp))
def trace_reset(self):
self.calls.append(('reset',))
a, b = T(), T()
fan = gsmtap.FanoutApduTracer([a, b])
fan.trace_command('00A4')
fan.trace_response('00A4', '9000', '')
fan.trace_reset()
self.assertEqual(a.calls, b.calls)
self.assertEqual(len(a.calls), 3)