# ria-ran--sim-drop **Closed-loop eSIM provisioning for emergency / NTN 5G deployments.** A self-contained appliance (Raspberry Pi or Android) that connects to a 5G gNB for backhaul, broadcasts an open WiFi AP, and hands out eSIM credentials to connecting devices through a captive portal. Users go from "no signal" to "connected to the ad-hoc 5G network" in under a minute, with no operator in the loop. > Deep design rationale lives in [`plan/design.md`](plan/design.md). This README > is the operational guide. Notable changes are tracked in > [`CHANGELOG.md`](CHANGELOG.md). ## Architecture ``` ┌─────────────────────────────────────────┐ │ Server Host (srsRAN gNB) │ │ - srsRAN gNB │ │ - Open5GS Core (or free5gc) │ │ - Subscriber database │ └─────────────┬───────────────────────────┘ │ 5G NR ▼ ┌─────────────────────────────────────────┐ │ UE WiFi AP Drop │ │ (Android device or Raspberry Pi) │ │ - WiFi AP (open network) │ │ - eSIM Provisioning Server (Flask) │ │ - Captive Portal (nginx + dnsmasq) │ └─────────────┬───────────────────────────┘ │ WiFi ▼ End User Devices ``` ## User flow 1. User connects to the open WiFi network `Emergency-5G`. 2. The captive portal auto-redirects to the activation page. 3. User taps **Activate eSIM**. 4. The server generates a unique IMSI / Ki / OPc / ICCID. 5. An eSIM profile is generated and offered as a one-time download. 6. User installs the profile via device Settings. 7. The device connects to the 5G network automatically. ## Quick start (local dev) ```bash git clone https://riahub.ai/qoherent/ria-ran--sim-drop.git cd ria-ran--sim-drop python3 -m venv venv && source venv/bin/activate pip install -r requirements.txt cp config.yaml.example config.yaml # edit MCC/MNC/op_key as needed python scripts/init_db.py python -m server.app # → http://localhost:5000 ``` Open and click **Activate eSIM**. ## Configuration All configuration is in `config.yaml` (start from `config.yaml.example`). | Field | Description | Example | |-------|-------------|---------| | `network.mcc` | Mobile Country Code (3 digits) | `"302"` | | `network.mnc` | Mobile Network Code (2–3 digits) | `"720"` | | `network.op_key` | Operator key, 16-byte hex (32 chars) | `"63bfa50e…"` | | `wifi.ip` | WiFi AP IP address | `"192.168.4.1"` | | `server.host` / `server.port` | Flask bind address | `"0.0.0.0"` / `5000` | | `server.admin_token` | Token guarding `/api/export`; empty = export disabled | `""` | | `database.path` | SQLite database path | `"/opt/…/subscribers.db"` | | `rate_limiting.window_hours` | One profile per MAC per window | `1` | | `profile.apn` | APN / DNN used in the profile and core session | `"internet"` | | `profile.smdp_address` | SM-DP+ address embedded in the LPA activation code | `"esim.local"` | | `core.type` | Core adapter: `open5gs` / `free5gc` / `none` | `"open5gs"` | | `core.mongodb_uri` | MongoDB connection for the core | `"mongodb://localhost:27017"` | `op_key` is validated at startup (must be 16-byte hex) and the config file is rejected if it is empty or missing a required section. ## API reference | Endpoint | Method | Description | |----------|--------|-------------| | `/activate` | GET | Captive portal page | | `/api/provision` | POST | Generate credentials + profile; returns a one-time `profile_url` | | `/api/profile/` | GET | Download the profile via its unguessable capability token | | `/api/export` | GET | Export all subscribers (JSON). Requires `X-Admin-Token`; disabled unless `server.admin_token` is set | | `/api/status` | GET | Health check (subscriber count, uptime, core adapter) | `POST /api/provision` accepts `{"mac_address": "aa:bb:cc:dd:ee:ff"}`; if omitted, the server resolves the client MAC from the ARP table. Rate limiting is per MAC. ## Profile format Two formats are selectable via `profile.format`: - **`v2`** (default, always available) — a JSON `profile-package-v2` bundle with the raw credentials plus **real 3GPP field encodings** (`EF_ICCID`, `EF_IMSI`, nibble-swapped BCD per 3GPP TS 31.102) and an SGP.22 **LPA activation code** (`LPA:1$$`). - **`saip`** — a real DER-encoded **SGP.22 SAIP `ProfileElement` sequence**: ProfileHeader (ICCID), PE-MF personalizing EF.ICCID, PE-USIM personalizing EF.IMSI (both against the standard "created by default" templates), and Milenage AKA parameters (Ki/OPc). Built with `asn1tools` against the official `PE_Definitions` ASN.1 schema. Enable by installing `requirements-saip.txt` (see that file for the schema source) and setting `profile.format: saip`. If prerequisites are missing the server logs a warning and falls back to `v2`. `saip_profile.decode_saip_profile()` parses a package back into its ProfileElements for inspection. The SAIP package carries ICCID, IMSI, and the AKA credentials; a fully installable profile additionally needs the remaining mandatory PEs (MF, PIN/PUK, full EF set) and card-level validation. Consumer over-the-air install also requires an SM-DP+ / LPA; these packages target programmable SIMs and core-network sync. See [`CHANGELOG.md`](CHANGELOG.md). ## Deployment ### Raspberry Pi Requirements: Pi 4 (4GB+), a compatible 5G modem (e.g. Quectel RM500Q), Raspberry Pi OS Lite 64-bit, Python 3.11+, and a pre-provisioned SIM for gNB backhaul. ```bash sudo bash scripts/setup_wifi_ap.sh ``` This installs hostapd/dnsmasq/nginx, configures the AP and captive-portal redirect, deploys the server under `/opt/esim-provisioning`, creates the `esim` service user, restricts `config.yaml` to `600`, initialises the DB, and enables the systemd services. Then edit `/etc/esim-provisioning/config.yaml` and `systemctl restart esim-provisioning`. ### Android / Termux ```bash bash android/termux_setup.sh ~/esim-provisioning/start.sh ``` See [`android/README.md`](android/README.md) for details and limitations. ## Core network sync With `core.type: none`, credentials are stored locally only. To push them to a core network: ```bash python scripts/sync_subscribers.py # sync unsynced subscribers to Open5GS/free5gc python scripts/export_subscribers.py # dump all subscribers as JSON (reads the DB directly) ``` To generate or inspect a profile outside the server (dev/ops): ```bash python scripts/make_profile.py --format saip --out profile.der # generate python scripts/make_profile.py --inspect profile.der # decode + summarize ``` Adding a new core is a single class in `server/core_adapters/` implementing the `CoreAdapter` interface. ## Security notes - Profiles are served at an unguessable one-time token URL, never an enumerable id — the profile body contains Ki/OPc. - `/api/export` returns all key material and is **disabled by default**; set `server.admin_token` and send it as `X-Admin-Token` to enable. - The deployed `config.yaml` (holds `op_key`) is `chmod 600`, owned by the service user; the systemd unit runs with `NoNewPrivileges`, `PrivateTmp`, and `ProtectSystem=strict`. - The WiFi network is intentionally open (emergency access); there is no transport encryption on the local link. ## Testing ```bash pip install -r requirements.txt -r requirements-dev.txt python -m pytest tests/ -q ``` The suite covers credential generation and 3GPP encodings, the database and rate limiting, the core adapters, config/utility validation, and the Flask API (provisioning, token download, rate-limit and export auth). ## Project layout ``` server/ Flask app, profile generator, database, core adapters portal/ Captive-portal frontend (HTML/CSS/JS) scripts/ init_db, sync/export subscribers, Pi AP setup configs/ nginx / dnsmasq / hostapd reference configs systemd/ Service units android/ Termux deployment tests/ Test suite plan/ Design doc and per-unit implementation notes ``` ## License MIT (or as appropriate for the deployment context).