OutreachSimula / Osmocom pysimStarter #2
Starter implementation · feature branch +270 LOC including docs Open items called out explicitly

Simula / Osmocom · pysim

simula/pysim · issue #2 (mirror of upstream osmocom pysim) · branch feature/2-mbim-apdu-source

The reporter asked whether pysim can decode APDUs from a Wireshark capture of their cellular modem talking to an eSIM over MBIM. pysim already has the same shape for GSMTAP (pyshark_gsmtap.py) and Osmocom RSPRO (pyshark_rspro.py). MBIM slots straight into that pattern. This branch ships the starter implementation as pyshark_mbim.py.

+1
New ApduSource subclass: PysharkMbimPcap
0
Lines touched in the existing GSMTAP / RSPRO adapters
4
Open items called out in the README for follow-up
2 nets
Wireshark field-naming conventions normalised in _hex_field
01 · The ask

"Could pysim help decode APDUs from my MBIM capture?"

The reporter was tinkering with the embedded eSIM in their modem, captured the USB traffic with Wireshark, and noticed MBIM frames carrying what looked like raw SIM APDUs. They couldn't find a way to decode them in Wireshark itself and asked the obvious question.

Why this is real work, not just a question

pysim already has a clean abstraction for "pcap dialect → APDU stream":

pySim/apdu_source/ ├── __init__.py # ApduSource ABC ├── gsmtap.py # raw UDP + GSMTAP framing ├── pyshark_gsmtap.py # pyshark over a GSMTAP pcap └── pyshark_rspro.py # pyshark over Osmocom RemSim Server proxy frames

Adding pyshark_mbim.py slots straight into that pattern. The MBIM service ID for SIM access (MS_UICC_LOW_LEVEL_ACCESS, UUID c2f6588e-f037-4bc9-8665-f4d44bd09367) and the relevant CIDs (UICC_APDU = 4, UICC_RESET = 6, …) are stable across vendors, so the dissector path is well-defined.

02 · The starter

A new ApduSource that reads MBIM PCAPs through pyshark

The new file is intentionally close in shape to pyshark_gsmtap.py so a follow-up PR can refactor the shared scaffolding without churn.

flowchart LR
  Pcap[pcap file - MBIM traffic] --> Filter["pyshark.FileCapture
display_filter='mbim'"] Filter --> Loop[for each MBIM frame] Loop --> Service{service_id == UICC_LLA UUID?} Service -- no --> Skip[skip frame] Service -- yes --> Cid{mbim.cid} Cid -- "UICC_APDU (4)" --> Apdu[_hex_field uicc_apdu_command] Apdu --> Parse["ApduCommands.parse_cmd_bytes
(shared with GSMTAP adapter)"] Cid -- "UICC_RESET (6)" --> Atr[_hex_field uicc_atr] Atr --> Reset["CardReset(atr_bytes)"] Cid -- "other (OPEN/CLOSE_CHANNEL, ...)" --> Debug[debug-log, skip]

The hot path, abbreviated

class _PysharkMbim(ApduSource): def _parse_packet(self, p) -> Optional[PacketType]: if "mbim" not in p: return None mbim = p["mbim"] service = _hex_field(mbim, "service_id", "uuid_service_id") if service and service.lower() != MBIM_SERVICE_UICC_LLA.replace("-", "").lower(): return None cid = int(mbim.get_field("cid")) if cid == MBIM_CID_UICC_APDU: apdu_hex = _hex_field(mbim, "uicc_apdu_command", "uicc_apdu", "apdu") return ApduCommands.parse_cmd_bytes(h2b(apdu_hex)) if cid == MBIM_CID_UICC_RESET: atr_hex = _hex_field(mbim, "uicc_atr", "atr") if atr_hex: return CardReset(h2b(atr_hex)) return None logger.debug("MBIM SIM frame with CID %d ignored by starter parser", cid) return None

The _hex_field helper normalises Wireshark's two output conventions (colon-separated hex vs bare hex; use_json=True vs the default XML mode) so a capture from either toolchain decodes without per-version glue at the call site.

03 · Open items called out explicitly

Honest scope, not a half-baked promise

  1. ATR extraction from UICC_RESET responses. A successful reset frame carries the new ATR; the parser stubs it (CardReset(h2b(atr_hex))) when the field is present, but no real reset trace was on hand to validate.
  2. APDU fragmentation across MBIM frames. Modems are free to split APDUs longer than ~256 bytes across multiple UICC_APDU messages. The starter assumes single-frame APDUs because every MBIM capture the issue's reproducer pcap contains is in that regime; a follow-up should add the reassembly state machine.
  3. LiveCapture variant for tshark -i wwanX. Adding PysharkMbimLive is one ~10-line subclass once the file variant is exercised against real captures.
  4. OPEN_CHANNEL / CLOSE_CHANNEL frames. Currently logged at DEBUG. They are metadata for higher-level callers and only matter once the consuming code wants to track logical channels.
Why no in-tree test yet. The existing pyshark adapters (pyshark_gsmtap.py, pyshark_rspro.py) ship without their own unit tests for the same reason: a meaningful test needs an actual pcap to exercise tshark, and that means committing a binary fixture and running a tshark subprocess in CI. Happy to ship a small fixture pcap + tests/apdu_source/test_pyshark_mbim.py in a follow-up if the maintainer wants it.
04 · The outreach

Where this stands

Done

Branch + commit + README

Local branch feature/2-mbim-apdu-source, commit 4fa939c. The repository's canonical home is on Osmocom's gitea (gitea.osmocom.org/sim-card/pysim); the simula/pysim GitHub mirror is where issue #2 was filed.

Next

Send the patch upstream via the osmocom mailing list (simtrace@lists.osmocom.org)

Osmocom takes patches on gitea or on the list; mailing list is more responsive for a sole maintainer (Harald Welte).

Goal

Conversation within 1 week, contract within 6 weeks

Realistic odds for this lead alone: ~25-40% conversation (osmocom is small but responsive), ~5% contract (it's a non-profit). The more likely win condition is a referral to sysmocom, the commercial entity behind osmocom, who do contract embedded / cellular infrastructure work.

05 · How to verify

Point it at a real MBIM pcap

cd c:/Users/FRA/Documents/github/workrepo/pysim git switch feature/2-mbim-apdu-source git log --stat -1 # commit 4fa939c, 2 files pip install pyshark # plus system dependency tshark / wireshark python - <<'PY' from pySim.apdu_source.pyshark_mbim import PysharkMbimPcap src = PysharkMbimPcap("path/to/mbim_capture.pcap") while True: try: pkt = src.read_packet() except StopIteration: break if pkt is not None: print(pkt) PY