Push Tracker
Go to file
Push Tracker
ash pushed to main at qoherent/ria-ran--sim-drop 2026-07-01 23:17:22 -04:00
244a6ab05e note
Files 20 items
A ashkan@beigi.net d58a0bf915 SAIP: declare used templates in header GFSTEList
Populate eUICC-Mandatory-GFSTEList with the MF and USIM template OIDs the
profile depends on (was empty). 109 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 02:57:02 +00:00
android Drop unused requests dependency; termux config parity 2026-07-02 02:49:56 +00:00
configs Add Phase-1 eSIM provisioning implementation 2026-07-01 05:56:31 +00:00
plan Add Phase-1 eSIM provisioning implementation 2026-07-01 05:56:31 +00:00
portal Harden: concurrency safety (db lock timeout, cache lock, retry, portal) 2026-07-01 06:16:25 +00:00
scripts Add make_profile.py dev/ops CLI (generate + inspect) 2026-07-02 02:48:32 +00:00
server SAIP: declare used templates in header GFSTEList 2026-07-02 02:57:02 +00:00
systemd Add Phase-1 eSIM provisioning implementation 2026-07-01 05:56:31 +00:00
tests SAIP: declare used templates in header GFSTEList 2026-07-02 02:57:02 +00:00
.coverage Test: cover client-IP/ARP resolution and SAIP schema lookup 2026-07-02 02:53:25 +00:00
.gitattributes Initial commit 2026-07-01 01:40:49 -04:00
.gitignore Add Phase-1 eSIM provisioning implementation 2026-07-01 05:56:31 +00:00
CHANGELOG.md Harden: cap request body size (MAX_CONTENT_LENGTH) 2026-07-02 02:51:29 +00:00
claude.md Add Phase-1 eSIM provisioning implementation 2026-07-01 05:56:31 +00:00
config.yaml.example Add real SGP.22 SAIP profile generation (optional) 2026-07-02 02:35:54 +00:00
Makefile Add Makefile for common dev tasks 2026-07-02 02:55:10 +00:00
pytest.ini Add real SGP.22 SAIP profile generation (optional) 2026-07-02 02:35:54 +00:00
README.md Split dev deps out of requirements.txt 2026-07-02 02:54:24 +00:00
requirements-dev.txt Split dev deps out of requirements.txt 2026-07-02 02:54:24 +00:00
requirements-saip.txt Add real SGP.22 SAIP profile generation (optional) 2026-07-02 02:35:54 +00:00
requirements.txt Split dev deps out of requirements.txt 2026-07-02 02:54:24 +00:00

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. This README is the operational guide. Notable changes are tracked in 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)

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 http://localhost:5000/activate 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 (23 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/<token> 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$<smdp>$<matching-id>).
  • 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.

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.

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 android/termux_setup.sh
~/esim-provisioning/start.sh

See 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:

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

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

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