A
4a911f02bc
requirements.txt is now runtime-only (Flask/cryptography/pymongo/pyyaml) so the Pi/Termux installers no longer pull pytest into production. Test tools move to requirements-dev.txt; README testing step installs both. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
206 lines
8.4 KiB
Markdown
206 lines
8.4 KiB
Markdown
# 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 (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/<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`](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).
|