diff --git a/frontend/help-ru.html b/frontend/help-ru.html index e2b703a..92b031f 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -482,14 +482,14 @@

Воспроизводит шаблоны записи реального телефона при смене сетевых условий (исследование трасс в projects/UICC_NAA.md): подключение EPS, потеря сервиса / ограниченный сервис, запрет роуминга, откат на 2G, приём SMS, перенастройка cell broadcast и AUTHENTICATE. По кнопке на сценарий; карта должна быть подключена.

-

Параметры (свёрнуты) задают оператора (поиск по мировому списку MCC/MNC с сервера плюс выбор случайного роуминг-оператора и кнопка «Домашняя сеть», заполняющая HPLMN карты из первой записи EF.HPLMNwAcT с откатом на IMSI), LAC/Cell ID/TAC/RAC, необязательные идентификаторы (пусто = случайно: TMSI, GUTI, KSI, KASME, Kc, счётчики NAS, алгоритм, RAND/AUTN), переключатели сценария (включая «Отказ: записать FPLMN») и число циклов/задержку. Журнал шагов показывает каждую запись, ENVELOPE и AUTHENTICATE с их SW. Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE — FPLMN записывается только сценарием постоянного отказа (TS 31.102 §4.2.16), а 5GS location-файлы не записываются; записи меняют карту и видны в последующих сравнениях снимков.

+

Параметры (свёрнуты) задают оператора (поиск по мировому списку MCC/MNC с сервера плюс выбор случайного роуминг-оператора и кнопка «Домашняя сеть», заполняющая HPLMN карты из первой записи EF.HPLMNwAcT с откатом на IMSI), LAC/Cell ID/TAC/RAC, необязательные идентификаторы (пусто = случайно: TMSI, GUTI, KSI, KASME, Kc, счётчики NAS, алгоритм, RAND/AUTN), переключатели сценария (включая «Отказ: записать FPLMN») и число циклов/задержку. Журнал шагов показывает каждую запись, ENVELOPE и AUTHENTICATE с их SW. Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE — FPLMN дописывается только сценарием постоянного отказа (TS 31.102 §4.2.16, без дубликатов), а подключение к сети из списка сначала очищает её запись (успешный ручной выбор, TS 23.122); 5GS location-файлы не записываются; записи меняют карту и видны в последующих сравнениях снимков.

Монитор сетевого состояния

Рядом с кнопками симуляции компактная панель «Сетевое состояние» показывает, что сейчас хранит карта и что было сэмулировано последним. В заголовке — сэмулированное состояние сервиса: Не определено, пока его не задаст сценарий или событие Location status, затем Обычный сервис (зелёный), Ограниченный сервис (жёлтый) или Нет сервиса (красный), с красной пометкой PLMN не разрешён, если location-файлы или EF.FPLMN указывают на отказ регистрации — плюс текущее местоположение: PLMN, страна и оператор (из необязательного мирового списка MCC/MNC, если он загружен), LAI/RAI/TAI и класс роуминга (Домашняя сеть, если PLMN совпадает с HPLMN; Эквивалентная домашней, если он есть в EF.EHPLMN; иначе Гостевая (роуминг)). Ниже — по одной компактной строке на контролируемый файл (IMSI, EHPLMN, SPDI, HPLMNwAcT, LOCI, PSLOCI, EPSLOCI, EPSNSC, CBMI, CBMIR, SMSstatus, FPLMN) с декодированной сводкой и признаком последнего обновления (init, write, read, refresh); при наведении — все декодированные поля; длинные списки PLMN сокращаются (EF.HPLMNwAcT показывает только первую сеть и пометку … +N, а технологии доступа — в подсказке). Панель читает файлы один раз при подключении карты (только если ICCID читается), обновляет их на месте по записанным симулятором байтам, перечитывает EF.IMSI после каждого сценария и события Location status (мульти-IMSI апплеты) и никогда не опрашивает карту — кнопка «Обновить» перечитывает все файлы по требованию.

diff --git a/frontend/help.html b/frontend/help.html index 4328bf9..82c05bb 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -482,14 +482,14 @@

Replays the write patterns a real phone performs when the network condition changes (trace study in projects/UICC_NAA.md): EPS attach, service loss / limited service, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration and AUTHENTICATE. One button per scenario; the card must be equipped.

-

Parameters (collapsed) provide the operator (searchable worldwide MCC/MNC list served from the server, plus a random roaming picker and a Home network button that fills the card’s HPLMN from EF.HPLMNwAcT’s first record, falling back to the IMSI), LAC/Cell ID/TAC/RAC, optional identity values (empty = random: TMSI, GUTI, KSI, KASME, Kc, NAS counts, algorithm, RAND/AUTN), the scenario toggles (including Rejection: write FPLMN) and the churn count/delay. The step log lists every write, ENVELOPE and AUTHENTICATE with its SW. Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent — FPLMN is written only by the permanent rejection scenario (TS 31.102 §4.2.16) and the 5GS location files are never written; the writes change the card and are visible to later snapshot comparisons.

+

Parameters (collapsed) provide the operator (searchable worldwide MCC/MNC list served from the server, plus a random roaming picker and a Home network button that fills the card’s HPLMN from EF.HPLMNwAcT’s first record, falling back to the IMSI), LAC/Cell ID/TAC/RAC, optional identity values (empty = random: TMSI, GUTI, KSI, KASME, Kc, NAS counts, algorithm, RAND/AUTN), the scenario toggles (including Rejection: write FPLMN) and the churn count/delay. The step log lists every write, ENVELOPE and AUTHENTICATE with its SW. Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent — FPLMN is appended only by the permanent rejection scenario (TS 31.102 §4.2.16, never duplicated) and an attach to a listed PLMN clears its entry first (successful manual selection, TS 23.122), and the 5GS location files are never written; the writes change the card and are visible to later snapshot comparisons.

Network state monitor

Next to the simulation buttons a compact Network state panel shows what the card currently holds and what was last simulated. Its header carries the simulated service stateUndefined until a scenario or a Location status event sets it, then Normal service (green), Limited service (amber) or No service (red), with a red PLMN not allowed marker when the location files or EF.FPLMN show a rejection — plus the current location: PLMN, country and operator (from the optional worldwide MCC/MNC list when loaded), the LAI/RAI/TAI, and the roaming class (Home when the PLMN equals the HPLMN, Home equivalent when it is in EF.EHPLMN, otherwise Guest). Below it, one compact line per monitored file (IMSI, EHPLMN, SPDI, HPLMNwAcT, LOCI, PSLOCI, EPSLOCI, EPSNSC, CBMI, CBMIR, SMSstatus, FPLMN) with its decoded summary and how it was last updated (init, write, read, refresh); hover for the full decoded fields — long PLMN lists are abbreviated (EF.HPLMNwAcT shows only the first network plus a … +N counter, with the access technologies in the tooltip). The panel reads the files once at equip (only when the ICCID was readable), updates them in place from the bytes the simulator wrote, re-reads EF.IMSI after every scenario and Location-status event (multi-IMSI applets) and never polls the card — use Refresh to re-read all files on demand.

diff --git a/frontend/index.html b/frontend/index.html index af2d1a9..874cddf 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -942,7 +942,7 @@
Network simulation -
Replays the write patterns a real phone performs on network-condition changes (EPS attach, service loss, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration, AUTHENTICATE). Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent; FPLMN is written only by the permanent "PLMN not allowed" rejection (TS 31.102 4.2.16), and the 5GS location files are never touched.
+
Replays the write patterns a real phone performs on network-condition changes (EPS attach, service loss, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration, AUTHENTICATE). Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent; FPLMN is appended only by the permanent "PLMN not allowed" rejection (TS 31.102 4.2.16, never duplicated), an attach to a listed PLMN clears its entry first (successful manual selection, TS 23.122), and the 5GS location files are never touched.
@@ -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.18'; +const SIMPLE_VERSION = '2.7.19'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -13268,7 +13268,7 @@ const LANG_RU = { 'No events configured.': 'Нет настроенных событий.', 'Fetched proactive commands:': 'Извлечённые проактивные команды:', 'Network simulation': 'Симуляция сети', - 'Replays the write patterns a real phone performs on network-condition changes (EPS attach, service loss, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration, AUTHENTICATE). Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent; FPLMN is written only by the permanent "PLMN not allowed" rejection (TS 31.102 4.2.16), and the 5GS location files are never touched.': 'Воспроизводит шаблоны записи реального телефона при смене сетевых условий (подключение EPS, потеря сервиса, запрет роуминга, откат на 2G, приём SMS, перенастройка cell broadcast, AUTHENTICATE). Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE; FPLMN записывается только при постоянном отказе «PLMN not allowed» (TS 31.102 4.2.16), а 5GS location-файлы не затрагиваются.', + 'Replays the write patterns a real phone performs on network-condition changes (EPS attach, service loss, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration, AUTHENTICATE). Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent; FPLMN is appended only by the permanent "PLMN not allowed" rejection (TS 31.102 4.2.16, never duplicated), an attach to a listed PLMN clears its entry first (successful manual selection, TS 23.122), and the 5GS location files are never touched.': 'Воспроизводит шаблоны записи реального телефона при смене сетевых условий (подключение EPS, потеря сервиса, запрет роуминга, откат на 2G, приём SMS, перенастройка cell broadcast, AUTHENTICATE). Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE; FPLMN дописывается только сценарием постоянного отказа (TS 31.102 4.2.16, без дубликатов), а подключение к сети из списка сначала очищает её запись (успешный ручной выбор, TS 23.122); 5GS location-файлы не затрагиваются.', 'Cold boot': 'Холодная загрузка', 'EPS attach': 'Подключение EPS', '2G attach': 'Подключение 2G', diff --git a/frontend/sw.js b/frontend/sw.js index 183137c..384f42e 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v221'; +const CACHE = 'simple-v222'; const URLS = [ 'index.html', 'help.html', diff --git a/pyproject.toml b/pyproject.toml index 03af9ef..6789266 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "2.7.18" +version = "2.7.19" 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 af0b273..6712014 100644 --- a/pysim_simple_server/netsim.py +++ b/pysim_simple_server/netsim.py @@ -232,9 +232,12 @@ def fplmn_entries(data_hex): def insert_fplmn(data_hex, plmn_hex): """Store a denied PLMN per TS 31.102 4.2.16: fill the first empty slot, otherwise shift the list left and append (the longest-held entry is - lost). Returns the full updated EF content.""" + lost). Returns the full updated EF content, or None when the PLMN is + already listed (a duplicate entry is meaningless).""" plmn = _norm_hex(plmn_hex, 3) entries = fplmn_entries(data_hex) + if plmn in entries: + return None if not entries: return plmn try: @@ -246,6 +249,17 @@ def insert_fplmn(data_hex, plmn_hex): return ''.join(entries) +def remove_fplmn(data_hex, plmn_hex): + """Clear every occurrence of a PLMN from EF.FPLMN (a successful manual + selection removes the entry, TS 23.122). Returns the full updated EF + content, or None when the PLMN is not listed.""" + plmn = _norm_hex(plmn_hex, 3) + entries = fplmn_entries(data_hex) + if plmn not in entries: + return None + return ''.join('FFFFFF' if e == plmn else e for e in entries) + + # ---- Ciphering keys and CB/SMS files ---- @@ -558,6 +572,27 @@ class NetSimRunner: if sw != '9000' or not data: data = 'FF' * (size or 12) new_data = insert_fplmn(data, plmn) + if new_data is None: + self._add('skip', file='fplmn', note='PLMN already listed') + return None + return self.write_binary('fplmn', new_data, pad=False, label='fplmn') + + def clear_fplmn(self, plmn_hex): + """A successful manual selection removes the PLMN from EF.FPLMN + (TS 23.122); every occurrence is cleared and nothing is written when + the PLMN is not listed.""" + try: + self._open('fplmn') + except StepError as e: + self._add('skip', file='fplmn', note=str(e)) + return None + data, sw = self.read_binary_current() + if sw != '9000' or not data: + self._add('skip', file='fplmn', note='read failed (SW %s)' % sw) + return None + new_data = remove_fplmn(data, plmn_hex) + if new_data is None: + return None return self.write_binary('fplmn', new_data, pad=False, label='fplmn') def read_record_current(self, record=1): @@ -607,6 +642,9 @@ class NetSimRunner: label='epsnsc', optional=True) def write_real_locations(self, status=ST_UPDATED): + # A successful attach is a manual selection of this PLMN: it is + # removed from EF.FPLMN first (TS 23.122). + self.clear_fplmn(self.plmn) self.write_binary('loci', build_loci( self.p('tmsi') or rand_hex(4), self.plmn, self.lac, status), label='loci', optional=True) diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 9f89bc6..1217e77 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -26,7 +26,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '2.7.18' +VERSION = '2.7.19' 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 52293a1..3ed93b0 100644 --- a/tests/test_netsim.py +++ b/tests/test_netsim.py @@ -81,6 +81,20 @@ class BuilderTests(unittest.TestCase): self.assertEqual(netsim.fplmn_entries('FF' * 9), ['FFFFFF', 'FFFFFF', 'FFFFFF']) self.assertEqual(netsim.fplmn_entries(''), []) + # an already listed PLMN is never stored twice + self.assertIsNone(netsim.insert_fplmn('00F110' + 'FF' * 9, '00F110')) + self.assertIsNone(netsim.insert_fplmn('AABBCC00F110112233445566', '00F110')) + + def test_fplmn_remove_clears_every_occurrence(self): + # duplicates (the same VPLMN can be listed more than once) all go + self.assertEqual(netsim.remove_fplmn('00F110' + '00F110' + 'FF' * 6, '00F110'), + 'FF' * 12) + # gaps in other positions are preserved, the list is not compacted + self.assertEqual(netsim.remove_fplmn('AABBCC00F110112233445566', '00F110'), + 'AABBCCFFFFFF112233445566') + # not listed -> no write + self.assertIsNone(netsim.remove_fplmn('AABBCC' + 'FF' * 9, '00F110')) + self.assertIsNone(netsim.remove_fplmn('', '00F110')) def test_parse_imsi_matches_the_pysim_vector(self): self.assertEqual(netsim.parse_imsi('082982608200002080'), @@ -263,7 +277,11 @@ FILES = { def make_runner(params=None, event_list=(3,), sleep=None): - lchan = FakeLchan(dict(FILES)) + # Copy the file infos too: tests seed per-file data and must not leak it + # into the next runner (FILES holds shared FakeFileInfo objects). + files = {fid: FakeFileInfo(f.size, f.record_len, f.num, f.data) + for fid, f in FILES.items()} + lchan = FakeLchan(files) app = SimpleNamespace(rs=SimpleNamespace(lchan=[lchan])) srv = FakeSrv() runner = netsim.NetSimRunner(srv, app, params=params, event_list=event_list, @@ -321,6 +339,37 @@ class RunnerTests(unittest.TestCase): epsnsc = [w for w in lchan.writes if w[1] == '6FE4'][0][2] self.assertTrue(epsnsc.startswith('A0348001078120' + 'FF' * 32)) + def test_roaming_denied_skips_a_duplicate_fplmn_entry(self): + runner, lchan, srv = make_runner() + lchan.files['6F7B'].data = '00F110' + 'FF' * 9 + out = runner.run('roaming_denied') + self.assertTrue(out['success']) + self.assertFalse(any(w[1] == '6F7B' for w in lchan.writes)) + self.assertTrue(any('already listed' in s.get('note', '') + for s in out['steps'])) + + def test_attach_clears_every_fplmn_occurrence_first(self): + runner, lchan, srv = make_runner() + # the same VPLMN listed twice (older runs appended it again) + lchan.files['6F7B'].data = '00F110' + '00F110' + 'FF' * 6 + out = runner.run('attach_eps') + self.assertTrue(out['success']) + fplmn = [w for w in lchan.writes if w[1] == '6F7B'] + self.assertEqual(len(fplmn), 1) + self.assertEqual(fplmn[0][2], 'FF' * 12) + # the clear happens before the successful location writes + idx_fplmn = next(i for i, w in enumerate(lchan.writes) if w[1] == '6F7B') + idx_loci = next(i for i, w in enumerate(lchan.writes) if w[1] == '6F7E') + self.assertLess(idx_fplmn, idx_loci) + for fid in ('6F7E', '6F73', '6FE3'): + self.assertIn(fid, [w[1] for w in lchan.writes]) + + def test_attach_without_a_listed_plmn_writes_no_fplmn(self): + runner, lchan, srv = make_runner() + out = runner.run('attach_eps') + self.assertTrue(out['success']) + self.assertFalse(any(w[1] == '6F7B' for w in lchan.writes)) + def test_roaming_denied_never_stores_the_home_plmn(self): runner, lchan, srv = make_runner() srv.net_state = {'files': {