net-sim: bundle the MCC/MNC list and hide MVNOs from the picker (v2.7.7)
The operator picker depended on a workspace file outside the repo
(<workspace>/samples/mcc-mnc-list.json), so a fresh clone showed
'Operator list not loaded'.
- bundle the list in the package (pysim_simple_server/data/
mcc-mnc-list.json, byte-identical to pbakondy/mcc-mnc-list master
commit 97bc1652, MIT) with license + provenance in
mcc-mnc-list.LICENSE; declared as package data so wheels carry it
- _default_mcc_mnc_list() now points at the bundled file;
--mcc-mnc-list still overrides it
- hide MVNO entries from the simulator picker (search + random): they
do not operate their own network. Detected via the `bands` field
('MVNO', 'Satellite MVNO', ...); the full list stays loaded so the
Network state panel still resolves operator names
- tests/test_mcc_mnc.py: bundled file ships/parses, marker variants,
search/random filters on fixtures and on the real list
- README/AGENTS updated; version trio 2.7.7; sw cache simple-v210
This commit is contained in:
@@ -595,7 +595,7 @@ The **Phone simulator** tab provides real-time CAT session interaction. It has t
|
||||
|
||||
Values persist on the server until restart. Apply → hex updates; Save → POSTs to server. The server will use these values to populate TERMINAL RESPONSE data for future PLI proactive commands.
|
||||
|
||||
**Network simulation** — replays the card-facing write patterns of a real phone on network-condition changes (trace study: `projects/UICC_NAA.md`): **Cold boot**, **EPS attach**, **2G attach**, **Service lost**, **Limited service**, **Roaming denied**, **Churn**, **SMS received**, **CB reconfig** and **AUTHENTICATE**. Each scenario sends the Location status event (only when the card subscribed to it), updates the EPS NAS context, location files, Kc and CB/SMS files exactly as observed, and logs every step with its SW. Parameters (collapsed) cover the operator (searchable worldwide MCC/MNC list served by the server, plus a random roaming picker), LAC/Cell ID/TAC/RAC, optional identity values (empty = random: TMSI, GUTI, KSI, KASME, Kc, NAS counts, algorithm, RAND/AUTN), scenario toggles and the churn count/delay. Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent; FPLMN and 5GS location files are never touched. The operator list comes from `--mcc-mnc-list` (default `<workspace>/samples/mcc-mnc-list.json`).
|
||||
**Network simulation** — replays the card-facing write patterns of a real phone on network-condition changes (trace study: `projects/UICC_NAA.md`): **Cold boot**, **EPS attach**, **2G attach**, **Service lost**, **Limited service**, **Roaming denied**, **Churn**, **SMS received**, **CB reconfig** and **AUTHENTICATE**. Each scenario sends the Location status event (only when the card subscribed to it), updates the EPS NAS context, location files, Kc and CB/SMS files exactly as observed, and logs every step with its SW. Parameters (collapsed) cover the operator (searchable worldwide MCC/MNC list served by the server, plus a random roaming picker), LAC/Cell ID/TAC/RAC, optional identity values (empty = random: TMSI, GUTI, KSI, KASME, Kc, NAS counts, algorithm, RAND/AUTN), scenario toggles and the churn count/delay. Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent; FPLMN and 5GS location files are never touched. The operator list is bundled (`pysim_simple_server/data/mcc-mnc-list.json`, MIT; MVNO entries are hidden — the picker lists real networks), and `--mcc-mnc-list` overrides it.
|
||||
|
||||
## SCP81
|
||||
|
||||
|
||||
+1
-1
@@ -565,7 +565,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
||||
|
||||
Значения сохраняются на сервере до перезапуска. Apply → hex обновляется; Save → POST на сервер.
|
||||
|
||||
**Симуляция сети** — воспроизводит шаблоны записи реального телефона при смене сетевых условий (исследование трасс: `projects/UICC_NAA.md`): **Холодная загрузка**, **Подключение EPS**, **Подключение 2G**, **Потеря сервиса**, **Ограниченный сервис**, **Роуминг запрещён**, **Серия переподключений**, **Принято SMS**, **Перенастройка CB** и **AUTHENTICATE**. Каждый сценарий отправляет событие Location status (только если карта на него подписана), обновляет контекст EPS NAS, location-файлы, Kc и файлы CB/SMS в точности как в трассах и журналирует каждый шаг с его SW. Параметры (свёрнуты) задают оператора (поиск по мировому списку MCC/MNC с сервера плюс случайный роуминг-оператор), LAC/Cell ID/TAC/RAC, необязательные идентификаторы (пусто = случайно: TMSI, GUTI, KSI, KASME, Kc, счётчики NAS, алгоритм, RAND/AUTN), переключатели и число циклов/задержку. Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE; FPLMN и 5GS location-файлы не затрагиваются. Список операторов задаётся `--mcc-mnc-list` (по умолчанию `<workspace>/samples/mcc-mnc-list.json`).
|
||||
**Симуляция сети** — воспроизводит шаблоны записи реального телефона при смене сетевых условий (исследование трасс: `projects/UICC_NAA.md`): **Холодная загрузка**, **Подключение EPS**, **Подключение 2G**, **Потеря сервиса**, **Ограниченный сервис**, **Роуминг запрещён**, **Серия переподключений**, **Принято SMS**, **Перенастройка CB** и **AUTHENTICATE**. Каждый сценарий отправляет событие Location status (только если карта на него подписана), обновляет контекст EPS NAS, location-файлы, Kc и файлы CB/SMS в точности как в трассах и журналирует каждый шаг с его SW. Параметры (свёрнуты) задают оператора (поиск по мировому списку MCC/MNC с сервера плюс случайный роуминг-оператор), LAC/Cell ID/TAC/RAC, необязательные идентификаторы (пусто = случайно: TMSI, GUTI, KSI, KASME, Kc, счётчики NAS, алгоритм, RAND/AUTN), переключатели и число циклов/задержку. Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE; FPLMN и 5GS location-файлы не затрагиваются. Список операторов входит в поставку (`pysim_simple_server/data/mcc-mnc-list.json`, MIT; записи MVNO скрыты — в списке только реальные сети), переопределяется `--mcc-mnc-list`.
|
||||
|
||||
## SCP81
|
||||
|
||||
|
||||
+1
-1
@@ -1413,7 +1413,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.6';
|
||||
const SIMPLE_VERSION = '2.7.7';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v209';
|
||||
const CACHE = 'simple-v210';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
+5
-2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "2.7.6"
|
||||
version = "2.7.7"
|
||||
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.
|
||||
@@ -15,4 +15,7 @@ requires-python = ">=3.8"
|
||||
pysim-simple-server = "pysim_simple_server.__main__:main"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["pysim_simple_server*"]
|
||||
include = ["pysim_simple_server*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
pysim_simple_server = ["data/*.json", "data/*.LICENSE"]
|
||||
@@ -29,11 +29,11 @@ def _default_web_dir():
|
||||
|
||||
|
||||
def _default_mcc_mnc_list():
|
||||
# Workspace default: the CC-BY-SA operator list lives outside the repo
|
||||
# (<workspace>/samples/mcc-mnc-list.json); clones can point elsewhere with
|
||||
# --mcc-mnc-list and the endpoint reports available=false when missing.
|
||||
repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
return os.path.normpath(os.path.join(repo, '..', 'samples', 'mcc-mnc-list.json'))
|
||||
# Bundled default: the MIT-licensed operator list ships with the package
|
||||
# (data/mcc-mnc-list.json, from pbakondy/mcc-mnc-list); --mcc-mnc-list
|
||||
# overrides it and the endpoint reports available=false when missing.
|
||||
pkg = os.path.dirname(os.path.abspath(__file__))
|
||||
return os.path.join(pkg, 'data', 'mcc-mnc-list.json')
|
||||
|
||||
|
||||
def main():
|
||||
@@ -47,7 +47,7 @@ def main():
|
||||
parser.add_argument('--web-dir', default=_default_web_dir(), metavar='PATH',
|
||||
help='Directory with the SIMple PWA static files to serve (default: <repo>/frontend)')
|
||||
parser.add_argument('--mcc-mnc-list', default=_default_mcc_mnc_list(), metavar='PATH',
|
||||
help='Worldwide MCC/MNC operator list (JSON) for the network-simulation operator picker (default: <workspace>/samples/mcc-mnc-list.json)')
|
||||
help='Worldwide MCC/MNC operator list (JSON) for the network-simulation operator picker (default: the bundled data/mcc-mnc-list.json)')
|
||||
parser.add_argument('--log-requests', action='store_true', default=False, help='Log request/response payloads to stderr')
|
||||
parser.add_argument('--sms-oa', default='12345', metavar='DIGITS',
|
||||
help='TP-Originating-Address (SMSC number) for the SMS-DELIVER TPDU (default: 12345)')
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
mcc-mnc-list.json — worldwide MCC/MNC operator list (incl. MVNO entries)
|
||||
|
||||
Source: https://github.com/pbakondy/mcc-mnc-list
|
||||
mcc-mnc-list.json, commit 97bc165252bb9bb5b37946898ec556c29a38feb8
|
||||
(2023-04-04); retrieved 2026-09-20
|
||||
md5 e63af9bc0beb651575a457d53c2e6fdd
|
||||
Data: derived from the Wikipedia article "Mobile country code"
|
||||
(https://en.wikipedia.org/wiki/Mobile_country_code)
|
||||
|
||||
The Network simulation operator picker hides entries whose `bands` field
|
||||
marks them as MVNO (they do not operate their own radio network); the full
|
||||
list is kept so the Network state panel can still resolve operator names.
|
||||
|
||||
--- upstream license -------------------------------------------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Peter Bakondy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '2.7.6'
|
||||
VERSION = '2.7.7'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -461,6 +461,15 @@ def _mcc_mnc_load(path):
|
||||
return data
|
||||
|
||||
|
||||
def _mcc_mnc_is_mvno(e):
|
||||
"""True for MVNO entries: `bands` marks them (e.g. 'MVNO', 'Satellite MVNO').
|
||||
|
||||
MVNOs do not operate their own radio network, so the network-simulation
|
||||
picker hides them; the full list is kept for operator-name resolution.
|
||||
"""
|
||||
return 'mvno' in str(e.get('bands') or '').lower()
|
||||
|
||||
|
||||
def _mcc_mnc_search(data, query, limit=50):
|
||||
"""Compact substring search over country/brand/operator/MCC/MNC."""
|
||||
q = (query or '').strip().lower()
|
||||
@@ -468,6 +477,8 @@ def _mcc_mnc_search(data, query, limit=50):
|
||||
return []
|
||||
out = []
|
||||
for e in data or []:
|
||||
if _mcc_mnc_is_mvno(e):
|
||||
continue
|
||||
hay = ' '.join(str(e.get(k) or '') for k in
|
||||
('countryName', 'countryCode', 'mcc', 'mnc', 'brand', 'operator')).lower()
|
||||
if q not in hay:
|
||||
@@ -482,9 +493,10 @@ def _mcc_mnc_search(data, query, limit=50):
|
||||
def _mcc_mnc_random(data, exclude=None):
|
||||
"""One random operator entry (optionally excluding 'mccmnc' digits)."""
|
||||
pool = [e for e in (data or [])
|
||||
if (str(e.get('mcc') or '') + str(e.get('mnc') or '')) != (exclude or '')]
|
||||
if (str(e.get('mcc') or '') + str(e.get('mnc') or '')) != (exclude or '')
|
||||
and not _mcc_mnc_is_mvno(e)]
|
||||
if not pool:
|
||||
pool = data or []
|
||||
pool = [e for e in (data or []) if not _mcc_mnc_is_mvno(e)]
|
||||
if not pool:
|
||||
return None
|
||||
e = random.choice(pool)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# coding=utf-8
|
||||
"""Tests for the bundled MCC/MNC operator list and the simulator picker.
|
||||
|
||||
The list ships inside the package (``data/mcc-mnc-list.json``, MIT, from
|
||||
pbakondy/mcc-mnc-list). The Network simulation picker hides MVNO entries
|
||||
(``bands`` marks them) because they do not operate their own network; the
|
||||
Network state panel still resolves operator names from the full list.
|
||||
"""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from pysim_simple_server import __main__, server
|
||||
|
||||
|
||||
def _entry(mcc, mnc, brand, bands, **kw):
|
||||
e = {'mcc': mcc, 'mnc': mnc, 'brand': brand, 'operator': brand + ' op',
|
||||
'countryName': 'Testland', 'countryCode': 'TL', 'status': 'Operational',
|
||||
'bands': bands}
|
||||
e.update(kw)
|
||||
return e
|
||||
|
||||
|
||||
class BundledMccMncListTests(unittest.TestCase):
|
||||
def test_default_path_is_bundled_and_parses(self):
|
||||
path = __main__._default_mcc_mnc_list()
|
||||
pkg_dir = os.path.dirname(os.path.abspath(server.__file__))
|
||||
self.assertTrue(os.path.abspath(path).startswith(pkg_dir + os.sep), path)
|
||||
self.assertTrue(os.path.isfile(path), path)
|
||||
data = server._mcc_mnc_load(path)
|
||||
self.assertIsInstance(data, list)
|
||||
self.assertGreater(len(data), 3000)
|
||||
for key in ('mcc', 'mnc', 'countryName', 'countryCode', 'brand',
|
||||
'operator', 'status', 'bands'):
|
||||
self.assertIn(key, data[0])
|
||||
|
||||
def test_bundled_list_has_mvno_entries_to_filter(self):
|
||||
data = server._mcc_mnc_load(__main__._default_mcc_mnc_list())
|
||||
mvno = [e for e in data if server._mcc_mnc_is_mvno(e)]
|
||||
self.assertGreater(len(mvno), 300)
|
||||
|
||||
def test_mvno_marker_variants(self):
|
||||
for bands in ('MVNO', 'Satellite MVNO', 'FULL MVNO', 'MVNO / LTE 800',
|
||||
'5G 3500 / MVNO', 'mvno'):
|
||||
self.assertTrue(server._mcc_mnc_is_mvno(_entry('262', '01', 'X', bands)),
|
||||
bands)
|
||||
for bands in ('LTE 1800', 'GSM 900 / LTE 800', '', None):
|
||||
self.assertFalse(server._mcc_mnc_is_mvno(_entry('262', '01', 'X', bands)),
|
||||
repr(bands))
|
||||
|
||||
|
||||
class MccMncFilterTests(unittest.TestCase):
|
||||
DATA = [
|
||||
_entry('262', '01', 'Telekom', 'GSM 900 / LTE 800'),
|
||||
_entry('262', '77', 'MVNO brand', 'MVNO'),
|
||||
_entry('262', '78', 'Sat MVNO', 'Satellite MVNO'),
|
||||
_entry('262', '79', 'Mentions MVNO', 'LTE 1800',
|
||||
notes='Used by MVNO Kartu As'),
|
||||
]
|
||||
|
||||
def test_search_skips_mvno_entries(self):
|
||||
res = server._mcc_mnc_search(self.DATA, '262')
|
||||
self.assertEqual([(r['mcc'], r['mnc']) for r in res],
|
||||
[('262', '01'), ('262', '79')])
|
||||
# the MVNO brands would match these queries without the filter, but the
|
||||
# notes-only MVNO mention (a real network) still matches 'mvno'
|
||||
self.assertEqual([(r['mcc'], r['mnc']) for r in server._mcc_mnc_search(self.DATA, 'mvno')],
|
||||
[('262', '79')])
|
||||
self.assertEqual(server._mcc_mnc_search(self.DATA, 'sat mvno'), [])
|
||||
|
||||
def test_search_real_list_hides_mvno_entry(self):
|
||||
data = server._mcc_mnc_load(__main__._default_mcc_mnc_list())
|
||||
mvno = next(e for e in data if server._mcc_mnc_is_mvno(e) and e.get('brand'))
|
||||
res = server._mcc_mnc_search(data, mvno['brand'], limit=10000)
|
||||
self.assertNotIn((mvno['mcc'], mvno['mnc']),
|
||||
[(r['mcc'], r['mnc']) for r in res])
|
||||
|
||||
def test_random_skips_mvno_entries(self):
|
||||
mixed = [_entry('262', '77', 'MVNO brand', 'MVNO'),
|
||||
_entry('262', '78', 'Sat MVNO', 'Satellite MVNO')]
|
||||
self.assertIsNone(server._mcc_mnc_random(mixed))
|
||||
data = self.DATA
|
||||
for _ in range(100):
|
||||
r = server._mcc_mnc_random(data)
|
||||
self.assertIn((r['mcc'], r['mnc']), [('262', '01'), ('262', '79')])
|
||||
# exclusion still applies within the non-MVNO pool
|
||||
for _ in range(50):
|
||||
self.assertEqual(server._mcc_mnc_random(data, exclude='26201')['mnc'],
|
||||
'79')
|
||||
|
||||
def test_random_real_list_never_picks_mvno(self):
|
||||
data = server._mcc_mnc_load(__main__._default_mcc_mnc_list())
|
||||
by_pair = {(e['mcc'], e['mnc']): e for e in data}
|
||||
for _ in range(100):
|
||||
r = server._mcc_mnc_random(data)
|
||||
self.assertFalse(server._mcc_mnc_is_mvno(by_pair[(r['mcc'], r['mnc'])]))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user