A
Add Phase-1 eSIM provisioning implementation
Full working closed-loop eSIM provisioning stack (spec in claude.md / plan/):
Flask API, SQLite subscriber store, credential/profile generator, Open5GS
and free5gc core adapters, captive portal, operational scripts, systemd
units, network configs, and a 44-test suite.
Profile generation currently returns a mock JSON profile; real SGP.22
generation via pySim is the planned Phase-2 upgrade.
Fixes two bugs found while getting the suite green:
- generate_imsi hardcoded a 10-digit MSIN, producing 16-digit IMSIs for a
3-digit MNC; MSIN length is now 15 - len(mcc) - len(mnc).
- check_rate_limit compared timestamps as strings across mismatched formats
(SQLite CURRENT_TIMESTAMP vs Python isoformat); both sides now normalized
via SQLite datetime() so the window check is chronologically correct.
Add .gitignore for venv, caches, runtime config.yaml, and databases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 01:56:31 -04:00
|
|
|
#!/bin/bash
|
|
|
|
|
# setup_wifi_ap.sh — Configure a Raspberry Pi as a WiFi AP + captive portal
|
|
|
|
|
# for the eSIM provisioning system.
|
|
|
|
|
# Run as root: sudo bash scripts/setup_wifi_ap.sh
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
[ "$(id -u)" = "0" ] || { echo "ERROR: Must run as root (sudo)."; exit 1; }
|
|
|
|
|
|
|
|
|
|
SSID="${ESIM_SSID:-Emergency-5G}"
|
|
|
|
|
CHANNEL="${ESIM_CHANNEL:-6}"
|
|
|
|
|
AP_IP="${ESIM_AP_IP:-192.168.4.1}"
|
|
|
|
|
DHCP_RANGE="${ESIM_DHCP_RANGE:-192.168.4.10,192.168.4.250,24h}"
|
|
|
|
|
INSTALL_DIR="${ESIM_INSTALL_DIR:-/opt/esim-provisioning}"
|
|
|
|
|
IFACE="${ESIM_IFACE:-wlan0}"
|
|
|
|
|
FLASK_PORT="${ESIM_FLASK_PORT:-5000}"
|
|
|
|
|
|
|
|
|
|
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
|
|
|
|
|
|
|
|
echo "=== eSIM Provisioning — WiFi AP Setup ==="
|
|
|
|
|
echo " SSID: $SSID"
|
|
|
|
|
echo " Channel: $CHANNEL"
|
|
|
|
|
echo " AP IP: $AP_IP"
|
|
|
|
|
echo " Interface: $IFACE"
|
|
|
|
|
echo " Install dir: $INSTALL_DIR"
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
# ---- 1. Install packages ----
|
|
|
|
|
echo "[1/8] Installing packages..."
|
|
|
|
|
apt-get update -qq
|
|
|
|
|
apt-get install -y hostapd dnsmasq nginx python3 python3-pip python3-venv
|
|
|
|
|
|
|
|
|
|
# ---- 2. Static IP on wlan0 ----
|
|
|
|
|
echo "[2/8] Configuring static IP on $IFACE..."
|
|
|
|
|
DHCPCD_CONF=/etc/dhcpcd.conf
|
|
|
|
|
if ! grep -q "interface $IFACE" "$DHCPCD_CONF" 2>/dev/null; then
|
|
|
|
|
cat >> "$DHCPCD_CONF" << EOF
|
|
|
|
|
|
|
|
|
|
interface $IFACE
|
|
|
|
|
static ip_address=$AP_IP/24
|
|
|
|
|
nohook wpa_supplicant
|
|
|
|
|
EOF
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ---- 3. hostapd ----
|
|
|
|
|
echo "[3/8] Configuring hostapd..."
|
|
|
|
|
cat > /etc/hostapd/hostapd.conf << EOF
|
|
|
|
|
interface=$IFACE
|
|
|
|
|
driver=nl80211
|
|
|
|
|
ssid=$SSID
|
|
|
|
|
utf8_ssid=1
|
|
|
|
|
hw_mode=g
|
|
|
|
|
channel=$CHANNEL
|
|
|
|
|
ieee80211n=1
|
|
|
|
|
wmm_enabled=1
|
|
|
|
|
auth_algs=1
|
|
|
|
|
ignore_broadcast_ssid=0
|
|
|
|
|
max_num_sta=50
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
sed -i 's|#DAEMON_CONF=.*|DAEMON_CONF="/etc/hostapd/hostapd.conf"|' /etc/default/hostapd
|
|
|
|
|
|
|
|
|
|
# ---- 4. dnsmasq ----
|
|
|
|
|
echo "[4/8] Configuring dnsmasq..."
|
|
|
|
|
# Back up original if present
|
|
|
|
|
[ -f /etc/dnsmasq.conf ] && mv /etc/dnsmasq.conf /etc/dnsmasq.conf.bak
|
|
|
|
|
cat > /etc/dnsmasq.conf << EOF
|
|
|
|
|
interface=$IFACE
|
|
|
|
|
bind-interfaces
|
|
|
|
|
dhcp-range=$DHCP_RANGE
|
|
|
|
|
address=/#/$AP_IP
|
|
|
|
|
no-resolv
|
|
|
|
|
dhcp-leasefile=/var/lib/misc/dnsmasq.leases
|
|
|
|
|
log-queries
|
|
|
|
|
log-dhcp
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
# ---- 5. nginx ----
|
|
|
|
|
echo "[5/8] Configuring nginx..."
|
|
|
|
|
cat > /etc/nginx/sites-available/esim-provisioning << EOF
|
|
|
|
|
server {
|
|
|
|
|
listen 80 default_server;
|
|
|
|
|
server_name _;
|
|
|
|
|
|
|
|
|
|
location = / {
|
|
|
|
|
return 302 http://$AP_IP/activate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location /activate {
|
|
|
|
|
alias $INSTALL_DIR/portal/;
|
|
|
|
|
index index.html;
|
|
|
|
|
try_files \$uri \$uri/ /index.html;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location /css/ { alias $INSTALL_DIR/portal/css/; }
|
|
|
|
|
location /js/ { alias $INSTALL_DIR/portal/js/; }
|
|
|
|
|
|
|
|
|
|
location /api/ {
|
|
|
|
|
proxy_pass http://127.0.0.1:$FLASK_PORT;
|
|
|
|
|
proxy_set_header Host \$host;
|
|
|
|
|
proxy_set_header X-Real-IP \$remote_addr;
|
|
|
|
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location / {
|
|
|
|
|
return 302 http://$AP_IP/activate;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
EOF
|
|
|
|
|
ln -sf /etc/nginx/sites-available/esim-provisioning /etc/nginx/sites-enabled/
|
|
|
|
|
rm -f /etc/nginx/sites-enabled/default
|
|
|
|
|
nginx -t
|
|
|
|
|
|
|
|
|
|
# ---- 6. IP forwarding ----
|
|
|
|
|
echo "[6/8] Enabling IP forwarding..."
|
|
|
|
|
sed -i 's|#net.ipv4.ip_forward=1|net.ipv4.ip_forward=1|' /etc/sysctl.conf
|
|
|
|
|
sysctl -w net.ipv4.ip_forward=1
|
|
|
|
|
|
|
|
|
|
# ---- 7. iptables — captive portal redirect ----
|
|
|
|
|
echo "[7/8] Configuring iptables..."
|
|
|
|
|
# Redirect all HTTP traffic from wlan0 clients to nginx (captive portal)
|
|
|
|
|
iptables -t nat -A PREROUTING -i "$IFACE" -p tcp --dport 80 -j DNAT \
|
|
|
|
|
--to-destination "$AP_IP:80" 2>/dev/null || true
|
|
|
|
|
# Persist iptables rules
|
|
|
|
|
apt-get install -y iptables-persistent
|
|
|
|
|
netfilter-persistent save
|
|
|
|
|
|
|
|
|
|
# ---- 8. Deploy server and services ----
|
|
|
|
|
echo "[8/8] Deploying server..."
|
|
|
|
|
mkdir -p "$INSTALL_DIR" /etc/esim-provisioning
|
|
|
|
|
|
|
|
|
|
cp -r "$REPO_DIR/server" "$INSTALL_DIR/"
|
|
|
|
|
cp -r "$REPO_DIR/portal" "$INSTALL_DIR/"
|
|
|
|
|
cp -r "$REPO_DIR/scripts" "$INSTALL_DIR/"
|
|
|
|
|
cp -r "$REPO_DIR/requirements.txt" "$INSTALL_DIR/"
|
|
|
|
|
|
|
|
|
|
[ -f /etc/esim-provisioning/config.yaml ] || \
|
|
|
|
|
cp "$REPO_DIR/config.yaml.example" /etc/esim-provisioning/config.yaml
|
|
|
|
|
|
|
|
|
|
# Create venv and install deps
|
|
|
|
|
python3 -m venv "$INSTALL_DIR/venv"
|
|
|
|
|
"$INSTALL_DIR/venv/bin/pip" install -q -r "$INSTALL_DIR/requirements.txt"
|
|
|
|
|
|
|
|
|
|
# Create system user
|
|
|
|
|
id esim &>/dev/null || useradd --system --no-create-home --shell /usr/sbin/nologin esim
|
|
|
|
|
|
A
2026-07-01 17:59:27 -04:00
|
|
|
# Restrict config — it holds the OP key and other secrets. Readable only by
|
|
|
|
|
# the service user, not world-readable.
|
|
|
|
|
chown esim:esim /etc/esim-provisioning/config.yaml
|
|
|
|
|
chmod 600 /etc/esim-provisioning/config.yaml
|
|
|
|
|
|
A
Add Phase-1 eSIM provisioning implementation
Full working closed-loop eSIM provisioning stack (spec in claude.md / plan/):
Flask API, SQLite subscriber store, credential/profile generator, Open5GS
and free5gc core adapters, captive portal, operational scripts, systemd
units, network configs, and a 44-test suite.
Profile generation currently returns a mock JSON profile; real SGP.22
generation via pySim is the planned Phase-2 upgrade.
Fixes two bugs found while getting the suite green:
- generate_imsi hardcoded a 10-digit MSIN, producing 16-digit IMSIs for a
3-digit MNC; MSIN length is now 15 - len(mcc) - len(mnc).
- check_rate_limit compared timestamps as strings across mismatched formats
(SQLite CURRENT_TIMESTAMP vs Python isoformat); both sides now normalized
via SQLite datetime() so the window check is chronologically correct.
Add .gitignore for venv, caches, runtime config.yaml, and databases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 01:56:31 -04:00
|
|
|
mkdir -p "$INSTALL_DIR/data"
|
|
|
|
|
|
|
|
|
|
# Install systemd services
|
|
|
|
|
cp "$REPO_DIR/systemd/esim-provisioning.service" /etc/systemd/system/
|
|
|
|
|
cp "$REPO_DIR/systemd/captive-portal.service" /etc/systemd/system/
|
|
|
|
|
systemctl daemon-reload
|
|
|
|
|
|
|
|
|
|
# Initialise DB
|
|
|
|
|
CONFIG_PATH=/etc/esim-provisioning/config.yaml \
|
|
|
|
|
"$INSTALL_DIR/venv/bin/python" "$INSTALL_DIR/scripts/init_db.py"
|
|
|
|
|
|
A
2026-07-01 17:59:27 -04:00
|
|
|
# Own data (incl. the DB file just created by root) as the service user, which
|
|
|
|
|
# runs with ReadWritePaths=.../data — otherwise the service cannot write it.
|
|
|
|
|
chown -R esim:esim "$INSTALL_DIR/data"
|
|
|
|
|
|
A
Add Phase-1 eSIM provisioning implementation
Full working closed-loop eSIM provisioning stack (spec in claude.md / plan/):
Flask API, SQLite subscriber store, credential/profile generator, Open5GS
and free5gc core adapters, captive portal, operational scripts, systemd
units, network configs, and a 44-test suite.
Profile generation currently returns a mock JSON profile; real SGP.22
generation via pySim is the planned Phase-2 upgrade.
Fixes two bugs found while getting the suite green:
- generate_imsi hardcoded a 10-digit MSIN, producing 16-digit IMSIs for a
3-digit MNC; MSIN length is now 15 - len(mcc) - len(mnc).
- check_rate_limit compared timestamps as strings across mismatched formats
(SQLite CURRENT_TIMESTAMP vs Python isoformat); both sides now normalized
via SQLite datetime() so the window check is chronologically correct.
Add .gitignore for venv, caches, runtime config.yaml, and databases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 01:56:31 -04:00
|
|
|
# Enable everything
|
|
|
|
|
systemctl unmask hostapd
|
|
|
|
|
systemctl enable hostapd dnsmasq nginx esim-provisioning captive-portal
|
|
|
|
|
systemctl restart hostapd dnsmasq nginx esim-provisioning
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "=== Setup Complete ==="
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Edit /etc/esim-provisioning/config.yaml to set your MCC/MNC/OP key."
|
|
|
|
|
echo "Then: systemctl restart esim-provisioning"
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Clients can connect to WiFi '$SSID' and will be redirected to the"
|
|
|
|
|
echo "eSIM activation portal at http://$AP_IP/activate"
|