ui: FPLMN manual-selection clear and duplicate guard (v2.7.19)

Attaching to a PLMN listed in EF.FPLMN used to write successful locations
anyway, i.e. it attached to a forbidden network.  Per TS 23.122 a
successful manual selection removes the entry, so the attach scenarios
clear it first:

- netsim.remove_fplmn() clears every occurrence of the PLMN (entries are
  not compacted; FFFFFF gaps stay); insert_fplmn() returns None when the
  PLMN is already listed, so roaming_denied no longer stores duplicates
  (the live card had '250-99, 250-99').
- clear_fplmn() step, called from write_real_locations() -> covers
  attach_eps, attach_2g and the sms_received location rewrite; the write
  is logged as a normal fplmn update_binary.
- tests: remove_fplmn duplicates/absent/gaps; roaming_denied duplicate
  skip; attach clears both occurrences before the location writes;
  make_runner now copies FakeFileInfo so seeded data does not leak
  between tests.
- help EN/RU and AGENTS updated.
This commit is contained in:
2026-09-21 01:14:45 +03:00
parent dfb7b694f9
commit 0446d2a93c
8 changed files with 101 additions and 14 deletions
+3 -3
View File
@@ -482,14 +482,14 @@
<p class="text-sm mb-2">Воспроизводит шаблоны записи реального телефона при смене сетевых условий (исследование трасс в <code class="font-mono text-sm">projects/UICC_NAA.md</code>): подключение EPS, потеря сервиса / ограниченный сервис, запрет роуминга, откат на 2G, приём SMS, перенастройка cell broadcast и AUTHENTICATE. По кнопке на сценарий; карта должна быть подключена.</p> <p class="text-sm mb-2">Воспроизводит шаблоны записи реального телефона при смене сетевых условий (исследование трасс в <code class="font-mono text-sm">projects/UICC_NAA.md</code>): подключение EPS, потеря сервиса / ограниченный сервис, запрет роуминга, откат на 2G, приём SMS, перенастройка cell broadcast и AUTHENTICATE. По кнопке на сценарий; карта должна быть подключена.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3"> <ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Холодная загрузка</strong> — инвалидация EPSNSC (KSI 07, ключ стирается) и фиктивные location-файлы.</li> <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>Подключение EPS / 2G</strong> — запись реального контекста EPS NAS (KSI, KASME, счётчики NAS, алгоритм) и реальных LOCI/PSLOCI/EPSLOCI (в 2G дополнительно реальные Kc/KcGPRS). Подключение считается успешным ручным выбором выбранного PLMN: если он есть в EF.FPLMN, его записи сначала очищаются (TS 23.122), поэтому подключение к запрещённой сети не проходит незаметно.</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> — событие 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>Серия переподключений</strong> — реальная → невалидная запись EPSNSC подряд (число циклов и задержка настраиваются).</li>
<li><strong>Принято SMS</strong> — инкремент счётчика EF.SMSstatus (чтение-изменение-запись) и, по желанию, перезапись location-файлов.</li> <li><strong>Принято SMS</strong> — инкремент счётчика EF.SMSstatus (чтение-изменение-запись) и, по желанию, перезапись location-файлов.</li>
<li><strong>Перенастройка CB</strong> — запись списков CBMI/CBMIR или их очистка (все FF).</li> <li><strong>Перенастройка CB</strong> — запись списков CBMI/CBMIR или их очистка (все FF).</li>
<li><strong>AUTHENTICATE</strong> — команда AUTHENTICATE (3G/EPS/5G, <code class="font-mono text-sm">00 88 00 81 22</code>) с заданными или случайными RAND/AUTN и показ ответа (успех <code class="font-mono text-sm">DB</code> или ошибка синхронизации <code class="font-mono text-sm">DC</code> с AUTS).</li> <li><strong>AUTHENTICATE</strong> — команда AUTHENTICATE (3G/EPS/5G, <code class="font-mono text-sm">00 88 00 81 22</code>) с заданными или случайными RAND/AUTN и показ ответа (успех <code class="font-mono text-sm">DB</code> или ошибка синхронизации <code class="font-mono text-sm">DC</code> с AUTS).</li>
</ul> </ul>
<p class="text-sm mb-3"><strong>Параметры</strong> (свёрнуты) задают оператора (поиск по мировому списку MCC/MNC с сервера плюс выбор случайного роуминг-оператора и кнопка <strong>&laquo;Домашняя сеть&raquo;</strong>, заполняющая HPLMN карты из первой записи EF.HPLMNwAcT с откатом на IMSI), LAC/Cell ID/TAC/RAC, необязательные идентификаторы (пусто = случайно: TMSI, GUTI, KSI, KASME, Kc, счётчики NAS, алгоритм, RAND/AUTN), переключатели сценария (включая <strong>&laquo;Отказ: записать FPLMN&raquo;</strong>) и число циклов/задержку. Журнал шагов показывает каждую запись, ENVELOPE и AUTHENTICATE с их SW. Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE — FPLMN записывается только сценарием постоянного отказа (TS 31.102 §4.2.16), а 5GS location-файлы не записываются; записи меняют карту и видны в последующих сравнениях снимков.</p> <p class="text-sm mb-3"><strong>Параметры</strong> (свёрнуты) задают оператора (поиск по мировому списку MCC/MNC с сервера плюс выбор случайного роуминг-оператора и кнопка <strong>&laquo;Домашняя сеть&raquo;</strong>, заполняющая HPLMN карты из первой записи EF.HPLMNwAcT с откатом на IMSI), LAC/Cell ID/TAC/RAC, необязательные идентификаторы (пусто = случайно: TMSI, GUTI, KSI, KASME, Kc, счётчики NAS, алгоритм, RAND/AUTN), переключатели сценария (включая <strong>&laquo;Отказ: записать FPLMN&raquo;</strong>) и число циклов/задержку. Журнал шагов показывает каждую запись, ENVELOPE и AUTHENTICATE с их SW. Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE — FPLMN дописывается только сценарием постоянного отказа (TS 31.102 §4.2.16, без дубликатов), а подключение к сети из списка сначала очищает её запись (успешный ручной выбор, TS 23.122); 5GS location-файлы не записываются; записи меняют карту и видны в последующих сравнениях снимков.</p>
<h4 id="network-state" class="font-medium mb-1">Монитор сетевого состояния</h4> <h4 id="network-state" class="font-medium mb-1">Монитор сетевого состояния</h4>
<p class="text-sm mb-3">Рядом с кнопками симуляции компактная панель <strong>&laquo;Сетевое состояние&raquo;</strong> показывает, что сейчас хранит карта и что было сэмулировано последним. В заголовке — <strong>сэмулированное состояние сервиса</strong>: <em>Не определено</em>, пока его не задаст сценарий или событие Location status, затем <em>Обычный сервис</em> (зелёный), <em>Ограниченный сервис</em> (жёлтый) или <em>Нет сервиса</em> (красный), с красной пометкой <em>PLMN не разрешён</em>, если location-файлы или EF.FPLMN указывают на отказ регистрации — плюс текущее <strong>местоположение</strong>: PLMN, страна и оператор (из необязательного мирового списка MCC/MNC, если он загружен), LAI/RAI/TAI и <strong>класс роуминга</strong> (<em>Домашняя сеть</em>, если PLMN совпадает с HPLMN; <em>Эквивалентная домашней</em>, если он есть в EF.EHPLMN; иначе <em>Гостевая (роуминг)</em>). Ниже — по одной компактной строке на контролируемый файл (IMSI, EHPLMN, SPDI, HPLMNwAcT, LOCI, PSLOCI, EPSLOCI, EPSNSC, CBMI, CBMIR, SMSstatus, FPLMN) с декодированной сводкой и признаком последнего обновления (<code class="font-mono text-sm">init</code>, <code class="font-mono text-sm">write</code>, <code class="font-mono text-sm">read</code>, <code class="font-mono text-sm">refresh</code>); при наведении — все декодированные поля; длинные списки PLMN сокращаются (EF.HPLMNwAcT показывает только первую сеть и пометку <code class="font-mono text-sm">… +N</code>, а технологии доступа — в подсказке). Панель читает файлы один раз при подключении карты (только если ICCID читается), обновляет их на месте по записанным симулятором байтам, перечитывает EF.IMSI после каждого сценария и события Location status (мульти-IMSI апплеты) и никогда не опрашивает карту — кнопка <strong>&laquo;Обновить&raquo;</strong> перечитывает все файлы по требованию.</p> <p class="text-sm mb-3">Рядом с кнопками симуляции компактная панель <strong>&laquo;Сетевое состояние&raquo;</strong> показывает, что сейчас хранит карта и что было сэмулировано последним. В заголовке — <strong>сэмулированное состояние сервиса</strong>: <em>Не определено</em>, пока его не задаст сценарий или событие Location status, затем <em>Обычный сервис</em> (зелёный), <em>Ограниченный сервис</em> (жёлтый) или <em>Нет сервиса</em> (красный), с красной пометкой <em>PLMN не разрешён</em>, если location-файлы или EF.FPLMN указывают на отказ регистрации — плюс текущее <strong>местоположение</strong>: PLMN, страна и оператор (из необязательного мирового списка MCC/MNC, если он загружен), LAI/RAI/TAI и <strong>класс роуминга</strong> (<em>Домашняя сеть</em>, если PLMN совпадает с HPLMN; <em>Эквивалентная домашней</em>, если он есть в EF.EHPLMN; иначе <em>Гостевая (роуминг)</em>). Ниже — по одной компактной строке на контролируемый файл (IMSI, EHPLMN, SPDI, HPLMNwAcT, LOCI, PSLOCI, EPSLOCI, EPSNSC, CBMI, CBMIR, SMSstatus, FPLMN) с декодированной сводкой и признаком последнего обновления (<code class="font-mono text-sm">init</code>, <code class="font-mono text-sm">write</code>, <code class="font-mono text-sm">read</code>, <code class="font-mono text-sm">refresh</code>); при наведении — все декодированные поля; длинные списки PLMN сокращаются (EF.HPLMNwAcT показывает только первую сеть и пометку <code class="font-mono text-sm">… +N</code>, а технологии доступа — в подсказке). Панель читает файлы один раз при подключении карты (только если ICCID читается), обновляет их на месте по записанным симулятором байтам, перечитывает EF.IMSI после каждого сценария и события Location status (мульти-IMSI апплеты) и никогда не опрашивает карту — кнопка <strong>&laquo;Обновить&raquo;</strong> перечитывает все файлы по требованию.</p>
+3 -3
View File
@@ -482,14 +482,14 @@
<p class="text-sm mb-2">Replays the write patterns a real phone performs when the network condition changes (trace study in <code class="font-mono text-sm">projects/UICC_NAA.md</code>): 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.</p> <p class="text-sm mb-2">Replays the write patterns a real phone performs when the network condition changes (trace study in <code class="font-mono text-sm">projects/UICC_NAA.md</code>): 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.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3"> <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>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>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). The attach is treated as a successful manual selection of the chosen PLMN: if it is listed in EF.FPLMN, its entries are cleared first (TS 23.122), so an attach never succeeds silently to a forbidden network.</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>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, never duplicated — an entry already listed is skipped) 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>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>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> <li><strong>CB reconfig</strong> — write the CBMI/CBMIR message-ID lists or clear them (all FF).</li>
<li><strong>AUTHENTICATE</strong> — send AUTHENTICATE (3G/EPS/5G, <code class="font-mono text-sm">00 88 00 81 22</code>) with the given or random RAND/AUTN and show the response (success <code class="font-mono text-sm">DB</code> or synchronisation failure <code class="font-mono text-sm">DC</code> with AUTS).</li> <li><strong>AUTHENTICATE</strong> — send AUTHENTICATE (3G/EPS/5G, <code class="font-mono text-sm">00 88 00 81 22</code>) with the given or random RAND/AUTN and show the response (success <code class="font-mono text-sm">DB</code> or synchronisation failure <code class="font-mono text-sm">DC</code> with AUTS).</li>
</ul> </ul>
<p class="text-sm mb-3"><strong>Parameters</strong> (collapsed) provide the operator (searchable worldwide MCC/MNC list served from the server, plus a random roaming picker and a <strong>Home network</strong> button that fills the card&rsquo;s HPLMN from EF.HPLMNwAcT&rsquo;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 <strong>Rejection: write FPLMN</strong>) 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.</p> <p class="text-sm mb-3"><strong>Parameters</strong> (collapsed) provide the operator (searchable worldwide MCC/MNC list served from the server, plus a random roaming picker and a <strong>Home network</strong> button that fills the card&rsquo;s HPLMN from EF.HPLMNwAcT&rsquo;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 <strong>Rejection: write FPLMN</strong>) 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.</p>
<h4 id="network-state" class="font-medium mb-1">Network state monitor</h4> <h4 id="network-state" class="font-medium mb-1">Network state monitor</h4>
<p class="text-sm mb-3">Next to the simulation buttons a compact <strong>Network state</strong> panel shows what the card currently holds and what was last simulated. Its header carries the <strong>simulated service state</strong><em>Undefined</em> until a scenario or a Location status event sets it, then <em>Normal service</em> (green), <em>Limited service</em> (amber) or <em>No service</em> (red), with a red <em>PLMN not allowed</em> marker when the location files or EF.FPLMN show a rejection — plus the current <strong>location</strong>: PLMN, country and operator (from the optional worldwide MCC/MNC list when loaded), the LAI/RAI/TAI, and the <strong>roaming class</strong> (<em>Home</em> when the PLMN equals the HPLMN, <em>Home equivalent</em> when it is in EF.EHPLMN, otherwise <em>Guest</em>). 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 (<code class="font-mono text-sm">init</code>, <code class="font-mono text-sm">write</code>, <code class="font-mono text-sm">read</code>, <code class="font-mono text-sm">refresh</code>); hover for the full decoded fields — long PLMN lists are abbreviated (EF.HPLMNwAcT shows only the first network plus a <code class="font-mono text-sm">… +N</code> 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 <strong>Refresh</strong> to re-read all files on demand.</p> <p class="text-sm mb-3">Next to the simulation buttons a compact <strong>Network state</strong> panel shows what the card currently holds and what was last simulated. Its header carries the <strong>simulated service state</strong><em>Undefined</em> until a scenario or a Location status event sets it, then <em>Normal service</em> (green), <em>Limited service</em> (amber) or <em>No service</em> (red), with a red <em>PLMN not allowed</em> marker when the location files or EF.FPLMN show a rejection — plus the current <strong>location</strong>: PLMN, country and operator (from the optional worldwide MCC/MNC list when loaded), the LAI/RAI/TAI, and the <strong>roaming class</strong> (<em>Home</em> when the PLMN equals the HPLMN, <em>Home equivalent</em> when it is in EF.EHPLMN, otherwise <em>Guest</em>). 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 (<code class="font-mono text-sm">init</code>, <code class="font-mono text-sm">write</code>, <code class="font-mono text-sm">read</code>, <code class="font-mono text-sm">refresh</code>); hover for the full decoded fields — long PLMN lists are abbreviated (EF.HPLMNwAcT shows only the first network plus a <code class="font-mono text-sm">… +N</code> 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 <strong>Refresh</strong> to re-read all files on demand.</p>
+3 -3
View File
@@ -942,7 +942,7 @@
<div class="flex flex-wrap items-start gap-4 mb-4"> <div class="flex flex-wrap items-start gap-4 mb-4">
<fieldset class="flex-1 border border-gray-200 dark:border-slate-700 rounded p-3" style="min-width:30rem"> <fieldset class="flex-1 border border-gray-200 dark:border-slate-700 rounded p-3" style="min-width:30rem">
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Network simulation">Network simulation</legend> <legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Network simulation">Network simulation</legend>
<div class="text-xs text-gray-500 dark:text-slate-400 mb-2" data-l10n="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 &quot;PLMN not allowed&quot; 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 written only by the permanent &quot;PLMN not allowed&quot; rejection (TS 31.102 4.2.16), and the 5GS location files are never touched.</div> <div class="text-xs text-gray-500 dark:text-slate-400 mb-2" data-l10n="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 &quot;PLMN not allowed&quot; 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.">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 &quot;PLMN not allowed&quot; 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.</div>
<div class="flex flex-wrap gap-1.5 mb-2"> <div class="flex flex-wrap gap-1.5 mb-2">
<button data-needs="card" onclick="netSimRun('cold_boot')" class="px-2.5 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Cold boot">Cold boot</button> <button data-needs="card" onclick="netSimRun('cold_boot')" class="px-2.5 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Cold boot">Cold boot</button>
<button data-needs="card" onclick="netSimRun('attach_eps')" class="px-2.5 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="EPS attach">EPS attach</button> <button data-needs="card" onclick="netSimRun('attach_eps')" class="px-2.5 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="EPS attach">EPS attach</button>
@@ -1414,7 +1414,7 @@
// ===== Version ===== // ===== Version =====
// Single source of truth for the PWA version: shown in the header and used // Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect(). // 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; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching ===== // ===== Tab switching =====
@@ -13268,7 +13268,7 @@ const LANG_RU = {
'No events configured.': 'Нет настроенных событий.', 'No events configured.': 'Нет настроенных событий.',
'Fetched proactive commands:': 'Извлечённые проактивные команды:', 'Fetched proactive commands:': 'Извлечённые проактивные команды:',
'Network simulation': 'Симуляция сети', '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': 'Холодная загрузка', 'Cold boot': 'Холодная загрузка',
'EPS attach': 'Подключение EPS', 'EPS attach': 'Подключение EPS',
'2G attach': 'Подключение 2G', '2G attach': 'Подключение 2G',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v221'; const CACHE = 'simple-v222';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pysim-simple-server" name = "pysim-simple-server"
version = "2.7.18" version = "2.7.19"
description = "HTTP REST server wrapping pysim for the SIMple PWA" description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8" requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+39 -1
View File
@@ -232,9 +232,12 @@ def fplmn_entries(data_hex):
def insert_fplmn(data_hex, plmn_hex): def insert_fplmn(data_hex, plmn_hex):
"""Store a denied PLMN per TS 31.102 4.2.16: fill the first empty slot, """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 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) plmn = _norm_hex(plmn_hex, 3)
entries = fplmn_entries(data_hex) entries = fplmn_entries(data_hex)
if plmn in entries:
return None
if not entries: if not entries:
return plmn return plmn
try: try:
@@ -246,6 +249,17 @@ def insert_fplmn(data_hex, plmn_hex):
return ''.join(entries) 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 ---- # ---- Ciphering keys and CB/SMS files ----
@@ -558,6 +572,27 @@ class NetSimRunner:
if sw != '9000' or not data: if sw != '9000' or not data:
data = 'FF' * (size or 12) data = 'FF' * (size or 12)
new_data = insert_fplmn(data, plmn) 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') return self.write_binary('fplmn', new_data, pad=False, label='fplmn')
def read_record_current(self, record=1): def read_record_current(self, record=1):
@@ -607,6 +642,9 @@ class NetSimRunner:
label='epsnsc', optional=True) label='epsnsc', optional=True)
def write_real_locations(self, status=ST_UPDATED): 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.write_binary('loci', build_loci(
self.p('tmsi') or rand_hex(4), self.plmn, self.lac, status), self.p('tmsi') or rand_hex(4), self.plmn, self.lac, status),
label='loci', optional=True) label='loci', optional=True)
+1 -1
View File
@@ -26,7 +26,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad 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 MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
+50 -1
View File
@@ -81,6 +81,20 @@ class BuilderTests(unittest.TestCase):
self.assertEqual(netsim.fplmn_entries('FF' * 9), self.assertEqual(netsim.fplmn_entries('FF' * 9),
['FFFFFF', 'FFFFFF', 'FFFFFF']) ['FFFFFF', 'FFFFFF', 'FFFFFF'])
self.assertEqual(netsim.fplmn_entries(''), []) 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): def test_parse_imsi_matches_the_pysim_vector(self):
self.assertEqual(netsim.parse_imsi('082982608200002080'), self.assertEqual(netsim.parse_imsi('082982608200002080'),
@@ -263,7 +277,11 @@ FILES = {
def make_runner(params=None, event_list=(3,), sleep=None): 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])) app = SimpleNamespace(rs=SimpleNamespace(lchan=[lchan]))
srv = FakeSrv() srv = FakeSrv()
runner = netsim.NetSimRunner(srv, app, params=params, event_list=event_list, 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] epsnsc = [w for w in lchan.writes if w[1] == '6FE4'][0][2]
self.assertTrue(epsnsc.startswith('A0348001078120' + 'FF' * 32)) 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): def test_roaming_denied_never_stores_the_home_plmn(self):
runner, lchan, srv = make_runner() runner, lchan, srv = make_runner()
srv.net_state = {'files': { srv.net_state = {'files': {