From 6bea10bb5b67023115383ae612e8f89c167b5aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 20 Sep 2026 13:30:26 +0300 Subject: [PATCH] net: align the EPSLOCI dummy with the corrected trace study (v2.7.2) The UICC_NAA.md C3/C3a recipes were re-checked against the corpus: the EPSLOCI dummy tail is FF*13 + `FF FE` + status - the TAC is always FFFE (never FF) and the status byte is 01 (not updated), not FF; the old `` reading was a misparse of that tail. The rejection form (status 02) is the C3a spec model: the only observed rejection trace never writes 6FE3 at all. - build_epsloci_dummy(status=ST_NOT_UPDATED, keep_plmn=None): corrected tail; keep_plmn selects the NMR style that preserves the last visited TAI PLMN (the guest style wipes GUTI and TAI PLMN) - both are observed - write_dummy_locations(): drop the all-FF special case (service loss now ends `FF FE 01`) and thread keep_plmn through - tests: exact guest/NMR/rejection vectors, 18-byte length, runner assertions for service_lost and roaming_denied; netstate fixture updated - UICC_NAA.md C3a tail ` 02` corrected to ` FF FE 02`; the PWA help/param text no longer claims the EPSLOCI dummy keeps the PLMN; SW cache simple-v205 --- frontend/help-ru.html | 2 +- frontend/help.html | 2 +- frontend/index.html | 6 +++--- frontend/sw.js | 2 +- pyproject.toml | 2 +- pysim_simple_server/netsim.py | 35 ++++++++++++++++++++--------------- pysim_simple_server/server.py | 2 +- tests/test_netsim.py | 17 ++++++++++++----- tests/test_netstate.py | 3 ++- 9 files changed, 42 insertions(+), 29 deletions(-) diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 8348b4f..495d631 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -483,7 +483,7 @@
  • Холодная загрузка — инвалидация EPSNSC (KSI 07, ключ стирается) и фиктивные location-файлы.
  • Подключение EPS / 2G — запись реального контекста EPS NAS (KSI, KASME, счётчики NAS, алгоритм) и реальных LOCI/PSLOCI/EPSLOCI (в 2G дополнительно реальные Kc/KcGPRS).
  • -
  • Потеря сервиса / ограниченный сервис / запрет роуминга — событие Location status (только если карта на него подписана), инвалидация EPSNSC (по желанию с сохранением старого KASME), фиктивные location-файлы (PLMN сохраняется, LAC FFFE, статус 01) и инвалидация Kc. Запрет роуминга эмулирует постоянный отказ «PLMN not allowed» (NAS cause #11): location-файлы получают статус 010 (EPSLOCI стирается до 0B F6 + статус), запрещённый VPLMN дописывается в EF.FPLMN по семантике сдвига из TS 31.102 §4.2.16 (домашняя сеть не записывается), ключевой контекст стирается.
  • +
  • Потеря сервиса / ограниченный сервис / запрет роуминга — событие Location status (только если карта на него подписана), инвалидация EPSNSC (по желанию с сохранением старого KASME), фиктивные location-файлы (LOCI/PSLOCI сохраняют PLMN, LAC FFFE, статус 01; EPSLOCI стирается до 0B F6 + FF×13 + FF FE 01) и инвалидация Kc. Запрет роуминга эмулирует постоянный отказ «PLMN not allowed» (NAS cause #11): location-файлы получают статус 010 (EPSLOCI 0B F6 + FF×13 + FF FE 02), запрещённый VPLMN дописывается в EF.FPLMN по семантике сдвига из TS 31.102 §4.2.16 (домашняя сеть не записывается), ключевой контекст стирается.
  • Серия переподключений — реальная → невалидная запись EPSNSC подряд (число циклов и задержка настраиваются).
  • Принято SMS — инкремент счётчика EF.SMSstatus (чтение-изменение-запись) и, по желанию, перезапись location-файлов.
  • Перенастройка CB — запись списков CBMI/CBMIR или их очистка (все FF).
  • diff --git a/frontend/help.html b/frontend/help.html index 9e5c2f3..be2abbd 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -483,7 +483,7 @@
    • Cold boot — invalidate EPSNSC (KSI 07, key wiped) and dummy the location files.
    • EPS attach / 2G attach — store a real EPS NAS context (KSI, KASME, NAS counts, algorithm) and write real LOCI/PSLOCI/EPSLOCI (2G also writes real Kc/KcGPRS).
    • -
    • Service lost / Limited service / Roaming denied — send the Location status event (only when the card subscribed to it), invalidate EPSNSC (optionally keeping the old KASME), dummy the location files (PLMN kept, LAC FFFE, status 01) and invalidate Kc. Roaming denied emulates a permanent “PLMN not allowed” rejection (NAS cause #11): the location files carry status 010 (EPSLOCI wiped to 0B F6 + status), the denied VPLMN is appended to EF.FPLMN with the shift-list semantics of TS 31.102 §4.2.16 (never the home PLMN) and the key context is dropped.
    • +
    • Service lost / Limited service / Roaming denied — send the Location status event (only when the card subscribed to it), invalidate EPSNSC (optionally keeping the old KASME), dummy the location files (the LOCI/PSLOCI keep the PLMN, LAC FFFE, status 01; EPSLOCI is wiped to 0B F6 + FF×13 + FF FE 01) and invalidate Kc. Roaming denied emulates a permanent “PLMN not allowed” rejection (NAS cause #11): the location files carry status 010 (EPSLOCI 0B F6 + FF×13 + FF FE 02), the denied VPLMN is appended to EF.FPLMN with the shift-list semantics of TS 31.102 §4.2.16 (never the home PLMN) and the key context is dropped.
    • Churn — replay real → invalid EPSNSC records back-to-back (count and delay configurable).
    • SMS received — bump the EF.SMSstatus counter (read-modify-write) and optionally rewrite the location files.
    • CB reconfig — write the CBMI/CBMIR message-ID lists or clear them (all FF).
    • diff --git a/frontend/index.html b/frontend/index.html index 10334ab..d6a7047 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -973,7 +973,7 @@ -
      Sets the PLMN written to LOCI/PSLOCI/EPSLOCI. Used by EPS attach, 2G attach and SMS received (real LAI/RAI/TAI; SMS only when its location rewrite is enabled), and by Cold boot, Service lost, Limited service and Roaming denied (dummy files keep this PLMN). Churn, CB reconfig and AUTHENTICATE don't use it.
      +
      Sets the PLMN written to LOCI/PSLOCI/EPSLOCI. Used by EPS attach, 2G attach and SMS received (real LAI/RAI/TAI; SMS only when its location rewrite is enabled), and by Cold boot, Service lost, Limited service and Roaming denied (the dummy LOCI/PSLOCI keep this PLMN, the EPSLOCI dummy wipes the TAI PLMN). Churn, CB reconfig and AUTHENTICATE don't use it.
      @@ -1414,7 +1414,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 = '2.7.1'; +const SIMPLE_VERSION = '2.7.2'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -13042,7 +13042,7 @@ const LANG_RU = { 'CB reconfig': 'Перенастройка CB', 'Parameters': 'Параметры', 'Operator search (country / brand / MCC-MNC)': 'Поиск оператора (страна / бренд / MCC-MNC)', - 'Sets the PLMN written to LOCI/PSLOCI/EPSLOCI. Used by EPS attach, 2G attach and SMS received (real LAI/RAI/TAI; SMS only when its location rewrite is enabled), and by Cold boot, Service lost, Limited service and Roaming denied (dummy files keep this PLMN). Churn, CB reconfig and AUTHENTICATE don\'t use it.': 'Задаёт PLMN, записываемый в LOCI/PSLOCI/EPSLOCI. Используется в сценариях «Подключение EPS», «Подключение 2G» и «Принято SMS» (реальные LAI/RAI/TAI; в SMS — только при включённой перезаписи location), а также «Холодная загрузка», «Потеря сервиса», «Ограниченный сервис» и «Роуминг запрещён» (фиктивные файлы сохраняют этот PLMN). «Серия переподключений», «Перенастройка CB» и AUTHENTICATE его не используют.', + 'Sets the PLMN written to LOCI/PSLOCI/EPSLOCI. Used by EPS attach, 2G attach and SMS received (real LAI/RAI/TAI; SMS only when its location rewrite is enabled), and by Cold boot, Service lost, Limited service and Roaming denied (the dummy LOCI/PSLOCI keep this PLMN, the EPSLOCI dummy wipes the TAI PLMN). Churn, CB reconfig and AUTHENTICATE don\'t use it.': 'Задаёт PLMN, записываемый в LOCI/PSLOCI/EPSLOCI. Используется в сценариях «Подключение EPS», «Подключение 2G» и «Принято SMS» (реальные LAI/RAI/TAI; в SMS — только при включённой перезаписи location), а также «Холодная загрузка», «Потеря сервиса», «Ограниченный сервис» и «Роуминг запрещён» (фиктивные LOCI/PSLOCI сохраняют этот PLMN, фиктивный EPSLOCI стирает TAI PLMN). «Серия переподключений», «Перенастройка CB» и AUTHENTICATE его не используют.', 'Random roaming operator': 'Случайный роуминг-оператор', 'Advanced identity (empty = random)': 'Дополнительные идентификаторы (пусто = случайно)', 'Algorithm': 'Алгоритм', diff --git a/frontend/sw.js b/frontend/sw.js index 53f61e6..16f9323 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v204'; +const CACHE = 'simple-v205'; const URLS = [ 'index.html', 'help.html', diff --git a/pyproject.toml b/pyproject.toml index e0b8e20..367b22a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "2.7.1" +version = "2.7.2" 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. diff --git a/pysim_simple_server/netsim.py b/pysim_simple_server/netsim.py index 3a094ec..af0b273 100644 --- a/pysim_simple_server/netsim.py +++ b/pysim_simple_server/netsim.py @@ -208,15 +208,18 @@ def build_epsloci(guti_hex, plmn_hex, tac_hex, status=ST_UPDATED): + bytes([status & 0xFF])).hex().upper() -def build_epsloci_dummy(status=None): +def build_epsloci_dummy(status=ST_NOT_UPDATED, keep_plmn=None): """EPSLOCI dummy: the EPS-mobile-identity pair `0B F6` (content length + - GUTI type octet) is kept, the GUTI/TAI/status bytes are wiped - (UICC_NAA.md C3). A rejection status (010 = roaming not allowed) is the - only byte written after the wipe (C3a).""" - out = '0BF6' + 'FF' * (15 if status is not None else 16) - if status is not None: - out += '%02X' % (status & 0xFF) - return out + GUTI type octet) is kept and the 12-byte GUTI is fully wiped; the TAC is + `FF FE` and the last byte is the EPS update status (`01` = not updated on + service loss, `02` = roaming not allowed on a permanent rejection - C3a, + the spec model; the corpus never captured a 6FE3 rejection write). + `keep_plmn` selects the NMR style that preserves the last visited TAI PLMN + instead of wiping it - the guest style wipes GUTI *and* TAI PLMN + (UICC_NAA.md 6.3/C3).""" + plmn = _norm_hex(keep_plmn, 3) if keep_plmn else None + tail = 'FF' * 10 + plmn if plmn else 'FF' * 13 + return '0BF6' + tail + 'FFFE' + '%02X' % (status & 0xFF) def fplmn_entries(data_hex): @@ -614,17 +617,19 @@ class NetSimRunner: self.p('guti') or rand_hex(12), self.plmn, self.tac, status), label='epsloci', optional=True) - def write_dummy_locations(self, status=ST_NOT_UPDATED): - """Service loss: LOCI/PSLOCI keep the PLMN with the dummy status 01; - EPSLOCI is wiped to `0B F6` + FF (UICC_NAA.md C3). A rejection status - (010 = PLMN not allowed) is written to all three; EPSLOCI then carries - that status byte as the only byte after the wipe (C3a).""" - eps_status = None if status == ST_NOT_UPDATED else status + def write_dummy_locations(self, status=ST_NOT_UPDATED, keep_plmn=None): + """Service loss: LOCI/PSLOCI keep the PLMN with the dummy status 01 + and EPSLOCI is wiped to `0B F6` + FF*13 + `FF FE 01` (UICC_NAA.md C3). + A rejection status (010 = PLMN not allowed) is written to all three + (EPSLOCI status 02 = roaming not allowed; C3a spec model). + `keep_plmn` keeps the last visited TAI PLMN in the EPSLOCI dummy + (NMR style) instead of wiping it.""" self.write_binary('loci', build_loci_dummy(self.plmn, status), label='loci', optional=True) self.write_binary('psloci', build_psloci_dummy(self.plmn, status), label='psloci', optional=True) - self.write_binary('epsloci', build_epsloci_dummy(eps_status), + self.write_binary('epsloci', + build_epsloci_dummy(status, keep_plmn=keep_plmn), label='epsloci', optional=True) def invalidate_kc(self): diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index bbe9264..87442b2 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -25,7 +25,7 @@ from osmocom.construct import GsmOrUcs2Adapter from osmocom.tlv import BER_TLV_IE -VERSION = '2.7.1' +VERSION = '2.7.2' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE diff --git a/tests/test_netsim.py b/tests/test_netsim.py index c97c31e..52293a1 100644 --- a/tests/test_netsim.py +++ b/tests/test_netsim.py @@ -56,12 +56,16 @@ class BuilderTests(unittest.TestCase): self.assertEqual(len(real) // 2, 18) self.assertEqual(real[24:30], '52F099') # TAI PLMN after the 12-byte GUTI self.assertEqual(real[-2:], '00') - # dummy: the EPS-mobile-identity pair 0B F6 stays, the rest is wiped + # dummy: 0B F6 + wiped GUTI/TAI-PLMN + TAC FFFE + status 01 (guest) dummy = netsim.build_epsloci_dummy() - self.assertEqual(dummy, '0BF6' + 'FF' * 16) - # permanent rejection: the status byte is the only written byte + self.assertEqual(len(dummy) // 2, 18) + self.assertEqual(dummy, '0BF6' + 'FF' * 13 + 'FFFE' + '01') + # NMR style: the last visited TAI PLMN is preserved, TAC FFFE + nmr = netsim.build_epsloci_dummy(keep_plmn='52F099') + self.assertEqual(nmr, '0BF6' + 'FF' * 10 + '52F099' + 'FFFE' + '01') + # permanent rejection: only the status byte differs (C3a spec model) rejected = netsim.build_epsloci_dummy(netsim.ST_PLMN_NOT_ALLOWED) - self.assertEqual(rejected, '0BF6' + 'FF' * 15 + '02') + self.assertEqual(rejected, '0BF6' + 'FF' * 13 + 'FFFE' + '02') def test_fplmn_insert_fills_empty_slots_and_shifts(self): # empty list: the new PLMN goes into the first slot @@ -295,6 +299,9 @@ class RunnerTests(unittest.TestCase): self.assertIn('4F20', keys) # Kc invalidate (07 form) kc = [w for w in lchan.writes if w[1] == '4F20'][0][2] self.assertEqual(kc, 'FFFFFFFFFFFFFFFF07') + # EPSLOCI dummy: 0B F6 + wiped GUTI/TAI-PLMN + TAC FFFE + status 01 + epsloci = [w for w in lchan.writes if w[1] == '6FE3'][0][2] + self.assertEqual(epsloci, '0BF6' + 'FF' * 13 + 'FFFE' + '01') def test_roaming_denied_writes_rejection_status_and_fplmn(self): runner, lchan, srv = make_runner() @@ -306,7 +313,7 @@ class RunnerTests(unittest.TestCase): psloci = [w for w in lchan.writes if w[1] == '6F73'][0][2] self.assertTrue(psloci.endswith('02'), psloci) epsloci = [w for w in lchan.writes if w[1] == '6FE3'][0][2] - self.assertEqual(epsloci, '0BF6' + 'FF' * 15 + '02') + self.assertEqual(epsloci, '0BF6' + 'FF' * 13 + 'FFFE' + '02') # the denied VPLMN 001-01 (00 F1 10) is appended to EF.FPLMN fplmn = [w for w in lchan.writes if w[1] == '6F7B'][0][2] self.assertEqual(fplmn, '00F110' + 'FF' * 9) diff --git a/tests/test_netstate.py b/tests/test_netstate.py index 94fd34b..ffc1777 100644 --- a/tests/test_netstate.py +++ b/tests/test_netstate.py @@ -61,7 +61,8 @@ class NetworkTests(unittest.TestCase): loci=file_entry('EF.LOCI', '6F7E', 'FFFFFFFF62F2106CD7FF01'), psloci=file_entry('EF.PSLOCI', '6F73', 'FFFFFFFFFFFFFF62F2106CD7CA01'), - epsloci=file_entry('EF.EPSLOCI', '6FE3', '0BF6' + 'FF' * 16)) + epsloci=file_entry('EF.EPSLOCI', '6FE3', + '0BF6' + 'FF' * 13 + 'FFFE' + '01')) loc = netstate._current_location(st['files']) # wiped EPSLOCI -> RAI from PSLOCI self.assertEqual(loc['plmn'], '26201')