101 lines
3.7 KiB
Python
101 lines
3.7 KiB
Python
##
|
|
## GSMTAP live streaming for the ISO 7816 decoder.
|
|
##
|
|
## Sends decoded events as GSMTAP-SIM UDP packets, compatible with
|
|
## simtrace2-sniff, SIMtrace Analyser and the GSMTAP dissector in
|
|
## Wireshark.
|
|
##
|
|
## This program is free software; you can redistribute it and/or modify
|
|
## it under the terms of the GNU General Public License as published by
|
|
## the Free Software Foundation; either version 2 of the License, or
|
|
## (at your option) any later version.
|
|
##
|
|
|
|
import socket
|
|
import struct
|
|
|
|
GSMTAP_VERSION = 0x02
|
|
GSMTAP_HDR_LEN = 4 # in 32-bit words (16 bytes)
|
|
GSMTAP_TYPE_SIM = 0x04
|
|
|
|
# sub_type values. 0x00-0x03 are standard GSMTAP-SIM types with standard
|
|
# payloads (libosmocore gsmtap.h): the PPS request and response are emitted
|
|
# as separate packets (PPS_REQ 0x02 / PPS_RSP 0x03). 0x10/0x11 are custom
|
|
# extensions (not in the spec) for RST/VCC line events.
|
|
GSMTAP_SIM_APDU = 0x00
|
|
GSMTAP_SIM_ATR = 0x01
|
|
GSMTAP_SIM_PPS_REQ = 0x02 # raw PPS request bytes
|
|
GSMTAP_SIM_PPS_RSP = 0x03 # raw PPS response bytes
|
|
GSMTAP_SIM_RST_EVENT = 0x10 # payload: [direction, level, reserved]
|
|
GSMTAP_SIM_VCC_EVENT = 0x11 # payload: [direction, level, reserved]
|
|
|
|
# RST/VCC event payload bytes:
|
|
# byte 0 (direction):
|
|
# RST: 1 = reset asserted (high->low), 0 = reset deasserted (low->high)
|
|
# VCC: 1 = power applied (low->high), 0 = power removed (high->low)
|
|
# byte 1 (level): line level after the transition (0 = low, 1 = high)
|
|
# byte 2: reserved (0x00)
|
|
|
|
GSMTAP_UDP_PORT = 4729
|
|
|
|
# Per-packet flags are carried in the header `res` byte, which libosmocore's
|
|
# gsmtap.h defines as reserved (RFU). This carriage is our own extension --
|
|
# upstream simtrace2-sniff/cardem never set `res` -- although the flag
|
|
# semantics mirror simtrace2's per-message USB data flags (the wire problem
|
|
# is reported out-of-band instead of being encoded in the APDU bytes).
|
|
GSMTAP_FLAG_BAD_FCS = 0x01 # "Any data checksum is wrong" -- reused here to
|
|
# mark decoder-desynced TPDUs (see README.md,
|
|
# "GSMTAP events").
|
|
|
|
_HDR_FMT = '!BBBBHBBIBBBB' # 16 bytes, big-endian
|
|
|
|
|
|
def build_packet(sub_type, data, flags=0):
|
|
'''Build a complete GSMTAP packet (header + payload) as bytes.
|
|
|
|
`flags` is written into the header `res` byte (GSMTAP_FLAG_*).'''
|
|
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 (libosmocore gsmtap.h field order)
|
|
0, # snr_db
|
|
0, # frame_number
|
|
sub_type, # sub_type
|
|
0, # antenna_nr
|
|
0, # sub_slot
|
|
flags, # res (GSMTAP_FLAG_*)
|
|
)
|
|
return hdr + bytes(data)
|
|
|
|
|
|
class GsmtapStreamSender:
|
|
'''Fire-and-forget UDP sender.
|
|
|
|
Never raises on send: with no listener present, datagrams are
|
|
silently dropped by the OS (same behavior as simtrace2-sniff).
|
|
'''
|
|
|
|
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)
|
|
# UDP to localhost sendto is non-blocking anyway; make it explicit so
|
|
# a full socket buffer can never stall the decode loop.
|
|
self._sock.setblocking(False)
|
|
|
|
def send(self, sub_type, data, flags=0):
|
|
try:
|
|
self._sock.sendto(build_packet(sub_type, data, flags),
|
|
self._addr)
|
|
except OSError:
|
|
pass
|
|
|
|
def close(self):
|
|
try:
|
|
self._sock.close()
|
|
except OSError:
|
|
pass
|