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
`<FF*16>` 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 `<FF*15> 02` corrected to `<FF*13> FF FE 02`;
  the PWA help/param text no longer claims the EPSLOCI dummy keeps the
  PLMN; SW cache simple-v205
This commit is contained in:
2026-09-20 13:30:26 +03:00
parent bb13ed45f8
commit 6bea10bb5b
9 changed files with 42 additions and 29 deletions
+1 -1
View File
@@ -483,7 +483,7 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Холодная загрузка</strong> — инвалидация EPSNSC (KSI 07, ключ стирается) и фиктивные location-файлы.</li>
<li><strong>Подключение EPS / 2G</strong> — запись реального контекста EPS NAS (KSI, KASME, счётчики NAS, алгоритм) и реальных LOCI/PSLOCI/EPSLOCI (в 2G дополнительно реальные Kc/KcGPRS).</li>
<li><strong>Потеря сервиса / ограниченный сервис / запрет роуминга</strong> — событие Location status (только если карта на него подписана), инвалидация EPSNSC (по желанию с сохранением старого KASME), фиктивные location-файлы (PLMN сохраняется, LAC <code class="font-mono text-sm">FFFE</code>, статус <code class="font-mono text-sm">01</code>) и инвалидация Kc. <strong>Запрет роуминга</strong> эмулирует постоянный отказ &laquo;PLMN not allowed&raquo; (NAS cause #11): location-файлы получают статус <code class="font-mono text-sm">010</code> (EPSLOCI стирается до <code class="font-mono text-sm">0B F6</code> + статус), запрещённый VPLMN дописывается в <strong>EF.FPLMN</strong> по семантике сдвига из TS 31.102 §4.2.16 (домашняя сеть не записывается), ключевой контекст стирается.</li>
<li><strong>Потеря сервиса / ограниченный сервис / запрет роуминга</strong> — событие Location status (только если карта на него подписана), инвалидация EPSNSC (по желанию с сохранением старого KASME), фиктивные location-файлы (LOCI/PSLOCI сохраняют PLMN, LAC <code class="font-mono text-sm">FFFE</code>, статус <code class="font-mono text-sm">01</code>; EPSLOCI стирается до <code class="font-mono text-sm">0B F6</code> + FF&times;13 + <code class="font-mono text-sm">FF FE 01</code>) и инвалидация Kc. <strong>Запрет роуминга</strong> эмулирует постоянный отказ &laquo;PLMN not allowed&raquo; (NAS cause #11): location-файлы получают статус <code class="font-mono text-sm">010</code> (EPSLOCI <code class="font-mono text-sm">0B F6</code> + FF&times;13 + <code class="font-mono text-sm">FF FE 02</code>), запрещённый VPLMN дописывается в <strong>EF.FPLMN</strong> по семантике сдвига из TS 31.102 §4.2.16 (домашняя сеть не записывается), ключевой контекст стирается.</li>
<li><strong>Серия переподключений</strong> — реальная → невалидная запись EPSNSC подряд (число циклов и задержка настраиваются).</li>
<li><strong>Принято SMS</strong> — инкремент счётчика EF.SMSstatus (чтение-изменение-запись) и, по желанию, перезапись location-файлов.</li>
<li><strong>Перенастройка CB</strong> — запись списков CBMI/CBMIR или их очистка (все FF).</li>
+1 -1
View File
@@ -483,7 +483,7 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Cold boot</strong> — invalidate EPSNSC (KSI 07, key wiped) and dummy the location files.</li>
<li><strong>EPS attach / 2G attach</strong> — store a real EPS NAS context (KSI, KASME, NAS counts, algorithm) and write real LOCI/PSLOCI/EPSLOCI (2G also writes real Kc/KcGPRS).</li>
<li><strong>Service lost / Limited service / Roaming denied</strong> — 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 <code class="font-mono text-sm">FFFE</code>, status <code class="font-mono text-sm">01</code>) and invalidate Kc. <strong>Roaming denied</strong> emulates a permanent &ldquo;PLMN not allowed&rdquo; rejection (NAS cause #11): the location files carry status <code class="font-mono text-sm">010</code> (EPSLOCI wiped to <code class="font-mono text-sm">0B F6</code> + status), the denied VPLMN is appended to <strong>EF.FPLMN</strong> with the shift-list semantics of TS 31.102 §4.2.16 (never the home PLMN) and the key context is dropped.</li>
<li><strong>Service lost / Limited service / Roaming denied</strong> — 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 <code class="font-mono text-sm">FFFE</code>, status <code class="font-mono text-sm">01</code>; EPSLOCI is wiped to <code class="font-mono text-sm">0B F6</code> + FF&times;13 + <code class="font-mono text-sm">FF FE 01</code>) and invalidate Kc. <strong>Roaming denied</strong> emulates a permanent &ldquo;PLMN not allowed&rdquo; rejection (NAS cause #11): the location files carry status <code class="font-mono text-sm">010</code> (EPSLOCI <code class="font-mono text-sm">0B F6</code> + FF&times;13 + <code class="font-mono text-sm">FF FE 02</code>), the denied VPLMN is appended to <strong>EF.FPLMN</strong> with the shift-list semantics of TS 31.102 §4.2.16 (never the home PLMN) and the key context is dropped.</li>
<li><strong>Churn</strong> — replay real → invalid EPSNSC records back-to-back (count and delay configurable).</li>
<li><strong>SMS received</strong> — bump the EF.SMSstatus counter (read-modify-write) and optionally rewrite the location files.</li>
<li><strong>CB reconfig</strong> — write the CBMI/CBMIR message-ID lists or clear them (all FF).</li>
+3 -3
View File
@@ -973,7 +973,7 @@
</div>
<button data-needs="server" onclick="netSimRandomOperator()" class="px-2 py-1 text-xs rounded bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Random roaming operator">Random roaming operator</button>
</div>
<div class="text-xs text-gray-400 dark:text-slate-500" data-l10n="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 (dummy files keep this PLMN). Churn, CB reconfig and AUTHENTICATE don't use it.</div>
<div class="text-xs text-gray-400 dark:text-slate-500" data-l10n="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.">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.</div>
<div id="netsim-op-results" class="text-xs font-mono max-h-32 overflow-auto"></div>
<div class="flex flex-wrap items-end gap-2">
<div>
@@ -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': 'Алгоритм',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v204';
const CACHE = 'simple-v205';
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 = "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.
+20 -15
View File
@@ -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):
+1 -1
View File
@@ -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
+12 -5
View File
@@ -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)
+2 -1
View File
@@ -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')