Push Tracker
ria-ran--sim-drop/README.md

196 lines
7.9 KiB
Markdown
Raw Normal View History

A
2026-07-01 01:40:49 -04: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`](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 <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 with ICCID + Milenage AKA parameters carrying 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`.
The SAIP package currently carries ICCID and the AKA credentials; emitting IMSI
and other EFs (USIM-template / GenericFileManagement PEs with File Control
Parameters) is the next layer. Note that consumer over-the-air install still
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)
```
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
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).