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')