"""simtrace2-pysniff — CLI entry point. Usage: python -m simtrace2_pysniff [options] """ import argparse import signal import sys import time from . import ( SniffSession, DeviceDisconnected, GsmtapSender, PcapWriter, FileDumper, format_message, __version__, ) from .dump import tpdu_timestamp_prefix, tpdu_atr_time_prefix from .gsmtap import build_gsmtap_packet, GSMTAP_SIM_ATR, GSMTAP_SIM_APDU def parse_gsmtap_addr(value): parts = value.rsplit(':', 1) host = parts[0] port = 4729 if len(parts) == 2: try: port = int(parts[1]) except ValueError: raise argparse.ArgumentTypeError( f'Invalid GSMTAP port: {parts[1]}') return host, port def build_parser(): p = argparse.ArgumentParser( prog='simtrace2-pysniff', description='Python-based SIMtrace2 sniffer with GSMTAP/PCAP output', ) p.add_argument('--version', action='version', version=f'simtrace2-pysniff {__version__}') p.add_argument('--gsmtap', type=parse_gsmtap_addr, metavar='HOST[:PORT]', help='Send ATR/TPDU as GSMTAP over UDP (default port 4729)') p.add_argument('--pcap', metavar='FILE', help='Write sniffed data to PCAP file for Wireshark') p.add_argument('--output', '-o', metavar='FILE', help='Write hex dump to file') p.add_argument('--no-stdout', action='store_true', help='Suppress hex dump to stdout') p.add_argument('--format', '-f', choices=['hex', 'timestamp', 'atr-time'], default='hex', help='Output format for TPDU lines (default: hex)') p.add_argument('--vendor', type=lambda x: int(x, 16), default=0x1d50, metavar='HEX', help='USB vendor ID (default: 0x1d50)') rec = p.add_argument_group('recovery options') rec.add_argument('--no-reconnect', action='store_true', help='Exit on USB disconnect instead of reconnecting') rec.add_argument('--reconnect-delay-min', type=float, default=1.0, metavar='SECONDS', help='Initial reconnect delay (default: 1.0)') rec.add_argument('--reconnect-delay-max', type=float, default=30.0, metavar='SECONDS', help='Maximum reconnect delay (default: 30.0)') rec.add_argument('--backoff-factor', type=float, default=1.5, metavar='N', help='Exponential backoff multiplier (default: 1.5)') rec.add_argument('--inactivity-timeout', type=float, default=0.0, metavar='SECONDS', help='Reconnect after N seconds of silence (default: disabled)') return p def print_banner(): print('simtrace2-pysniff — Phone-SIM card communication sniffer\n' f'Version {__version__} — Python-based replacement for simtrace2-sniff\n', file=sys.stderr) print('Entering main loop', file=sys.stderr) def main(): parser = build_parser() args = parser.parse_args() print_banner() gsmtap = None if args.gsmtap: host, port = args.gsmtap gsmtap = GsmtapSender(host, port) print(f'GSMTAP output: {host}:{port}', file=sys.stderr) pcap = None if args.pcap: pcap = PcapWriter(args.pcap) print(f'PCAP output: {args.pcap}', file=sys.stderr) file_dumper = None if args.output: file_dumper = FileDumper(args.output, fmt=args.format) print(f'File output: {args.output}', file=sys.stderr) session = SniffSession( reconnect=not args.no_reconnect, reconnect_delay_min=args.reconnect_delay_min, reconnect_delay_max=args.reconnect_delay_max, backoff_factor=args.backoff_factor, inactivity_timeout=args.inactivity_timeout, vendor_id=args.vendor, ) exiting = False def on_signal(signum, frame): nonlocal exiting if exiting: raise SystemExit(1) exiting = True print('\nShutting down...', file=sys.stderr) session.close() if gsmtap: gsmtap.close() if pcap: pcap.close() if file_dumper: file_dumper.close() sys.exit(0) if not sys.stdin.isatty(): signal.signal(signal.SIGHUP, signal.SIG_IGN) signal.signal(signal.SIGINT, on_signal) signal.signal(signal.SIGTERM, on_signal) atr_time_start = time.monotonic() try: for msg in session.iter_messages(): if msg.type == 'atr': atr_time_start = msg.timestamp tpdu_prefix = '' if args.format == 'timestamp' and msg.type == 'tpdu': tpdu_prefix = tpdu_timestamp_prefix(msg) elif args.format == 'atr-time' and msg.type == 'tpdu': tpdu_prefix = tpdu_atr_time_prefix( msg.timestamp - atr_time_start) if not args.no_stdout: print(format_message(msg, tpdu_prefix=tpdu_prefix)) if file_dumper is not None: file_dumper.write(msg) if gsmtap is not None and msg.type in ('atr', 'tpdu'): if msg.type == 'atr': gsmtap.send_atr(msg.data, msg.slot_nr) else: gsmtap.send_apdu(msg.data, msg.slot_nr) if pcap is not None and msg.type in ('atr', 'tpdu'): sub_type = GSMTAP_SIM_ATR if msg.type == 'atr' else GSMTAP_SIM_APDU gsmtap_hdr = build_gsmtap_packet(sub_type, b'')[:16] pcap.write_packet(gsmtap_hdr, msg.data, msg.timestamp) except DeviceDisconnected as e: print(f'Device disconnected: {e}', file=sys.stderr) except KeyboardInterrupt: pass finally: session.close() if gsmtap: gsmtap.close() if pcap: pcap.close() if file_dumper: file_dumper.close() if __name__ == '__main__': main()