# SPR — Secure Programmable Router (full LLM context) This file is a self-contained brief about SPR for LLMs. It explains what SPR is, the security model, how to set it up, and how to use the REST API. Where deeper detail is needed, links point at the canonical docs. Canonical home: https://www.supernetworks.org Source code: https://github.com/spr-networks/super API reference: https://www.supernetworks.org/pages/api/0 Discord: https://discord.gg/EUjTKJPPAX ---------------------------------------------------------------------- 1. What SPR is ---------------------------------------------------------------------- SPR (Secure Programmable Router) is an open-source, self-hosted WiFi router and firewall. Every device that joins the network is automatically placed in its own private network with its own WiFi passphrase. Devices cannot see or talk to each other unless an explicit policy permits it. There is no cloud component and no telemetry. SPR is engineered with security in mind at every step. Its defense-in-depth architecture combines independent controls—including per-device layer-2 isolation, default-deny packet filtering, container boundaries, and narrow service and privilege boundaries—so protection does not depend on any single layer. The project minimizes exposed services and attack surface, and SPR-authored backend components use memory-safe languages, primarily Go, wherever practical. SPR is delivered as Docker containers (Go services + React frontend) that run on Linux. It manages hostapd, nftables, DHCP, DNS, Wireguard, and a multicast proxy. A prebuilt Raspberry Pi image (`spr-debian.img.xz`) is published on every release. Containers are pinned per release, and the device runs entirely from local config files under `/home/spr/super/configs/`. A commercial offering exists: a $399.99 PoE+ WiFi 7 router built on the Raspberry Pi CM5 with an MT7996 3x3 802.11be radio and 2.5 Gbps Ethernet (https://www.supernetworks.org/compute-board.html). The same software runs on Raspberry Pi 4/5, Banana Pi, x86 Linux, or in any Docker host. ---------------------------------------------------------------------- 2. Security model ---------------------------------------------------------------------- Defense in depth — SPR layers network isolation, default-deny firewalling, container separation, explicit service interfaces, and least-privilege boundaries. A single failed or misconfigured control should not remove every other protection. Minimal attack surface — the architecture aims to expose only the services and privileged interfaces required to route and administer the network. Internal services communicate over local Unix sockets where possible. Memory-safe where practical — SPR-authored backend services are predominantly written in Go. SPR still relies on essential Linux networking and external system components such as hostapd and nftables, so this is a deliberate engineering preference rather than an absolute claim about every dependency. Default deny — the firewall drops forwarded and input packets unless an explicit rule allows them. Devices must be configured for internet, DNS, and intranet access. Per-device WiFi passphrases — every device gets a unique WPA2/WPA3 passphrase. SPR is the first project to support multiple passphrases on a single SSID with WPA3. Per-device VLANs and GTKs — every connected station is bridged into its own AP/VLAN virtual interface with its own group temporal key. Frames cannot cross between stations on layer 2. Default-deny multicast / no MACStealer — there is no shared L2 segment between clients to steal frames from. Multicast traffic is relayed by a userland proxy with explicit policy. Zero-trust identity — device identity = WiFi passphrase, VLAN tag, or Wireguard key. Policies are written against device identity rather than IP/subnet. NDSS 2026 (AirSnitch) — SPR's defaults stop the four cross-client WiFi attacks shown in the paper: shared GTK abuse, gateway bouncing, port stealing via MAC spoofing, and broadcast reflection. This has been the default SPR posture since 2022. Background: https://www.supernetworks.org/pages/blog/airsnitch-breaking-wifi-client-isolation ---------------------------------------------------------------------- 3. Architecture ---------------------------------------------------------------------- SPR is a set of Docker containers orchestrated by `docker compose`: - `api` — the main REST API server (Go), exposed on `:80` (HTTP) or `:443` (HTTPS) - `wifid` — bridge to hostapd, manages stations and passphrases - `dhcp` — coredhcp-based DHCP server with tinysubnets plugin - `dns` — CoreDNS with blocklists and per-device DNS rules - `wireguard` — VPN server - `multicast_proxy` — userland mDNS / SSDP relay - `superd` — supervises container restarts via a unix socket - `frontend` — React UI Internal APIs talk to each other over unix sockets in `/state/`. The external API is what you call from clients. Configuration lives in `/home/spr/super/configs/`. The most important files: - `configs/base/config.sh` — main config (WAN interface, LAN subnet, etc.) - `configs/base/dhcp.json` — DHCP settings - `configs/base/lanip` — LAN gateway IP - `configs/auth/auth_users.json` — admin web users - `configs/auth/auth_tokens.json` — API tokens - `configs/auth/otp_settings.json`— OTP / TOTP config - `configs/wifi/` — hostapd configs - `configs/dns/` — CoreDNS Corefile and blocklists Plugin isolation and authentication: - Runtime isolation — plugins can set `Runtime: "kvm"` to run under KVM with `spr-krun`. Each KVM plugin has its own Linux kernel rather than sharing the SPR host kernel like a standard container. Networking software frequently parses attacker-controlled packets and may need elevated networking privileges, so this adds a stronger boundary against privilege-escalation and container-escape vulnerabilities. - Sandboxed UI default — plugin UIs run in sandboxed iframes unless the plugin explicitly sets `SandboxedUI: false`. A sandboxed UI does not inherit the admin page's origin and does not receive the user's credential or a general-purpose SPR API token. - Ephemeral UI session — after authenticating the user, the parent SPR frontend obtains a short-lived random token for the iframe. It is scoped exactly to that plugin's `/plugins/` route and child paths. It cannot access regular SPR API routes, other plugins, token management, or WebSockets. The API removes the bearer header before proxying the request to the plugin's Unix socket. - Rekeying — the parent renews the UI session before expiry. If an API restart invalidates the in-memory session, an `invalid_token` response causes the iframe to request a rekey from the authenticated parent and retry once. Version mismatches and insufficient scopes fail closed. - Backend install tokens — if the plugin backend must call other SPR API routes, it can declare `InstallTokenPath` and least-privilege `ScopedPaths`. Do not configure an install token if the backend does not use the SPR API. Prefer `:r` read-only scopes and use `:rw` only for required mutations. Install tokens must never be exposed to browser code. - Legacy UI access — `SandboxedUI: false` disables the iframe sandbox and lets the plugin UI use the signed-in user's direct SPR API access. SPR warns the user before opening such a UI. This is a compatibility mode for trusted legacy plugins, not the recommended default. Plugin documentation: - https://www.supernetworks.org/pages/docs/guides/plugins - https://www.supernetworks.org/pages/docs/development/plugin-dev - https://www.supernetworks.org/pages/docs/development/apis/overview#authenticated-plugin-proxy ---------------------------------------------------------------------- 4. Setup (the three paths) ---------------------------------------------------------------------- A. Prebuilt SPR image (easiest, Raspberry Pi 4/5 or CM5): 1. Download spr-debian.img.xz from https://github.com/spr-networks/super/releases/latest 2. Flash with Raspberry Pi Imager, or: xzcat spr-debian.img.xz | dd of=/dev/rdiskX bs=$((1024*1024)) 3. Boot the device, connect a WAN ethernet cable 4. Either: - Browser: connect to the open AP `spr-setup` (or `sprlab-setup` on Pi), visit https://192.168.2.1 (or https://spr.local) and follow the setup wizard - SSH/TUI: `ssh ubuntu@192.168.2.1` or `ssh ubuntu@spr.local` (default password: `ubuntu`), then run `sudo /home/spr/super/base/scripts/spr-setup-tui.sh` B. Docker on existing Linux: git clone https://github.com/spr-networks/super cd super sudo base/setup.sh # or base/setup-sdcard.sh on SD card cp -R base/template_configs configs ./configs/scripts/gen_coredhcp_yaml.sh > configs/dhcp/coredhcp.yml ./configs/scripts/gen_watchdog.sh > configs/watchdog/watchdog.conf # edit configs/base/config.sh — set WANIF, LAN subnet, etc. docker compose up -d Full guide: https://www.supernetworks.org/pages/docs/setup_guides/setup_run_spr C. Virtual SPR (VPN-only, no WiFi): runs in a Linux netns or VM as a Wireguard gateway with the same isolation model. https://www.supernetworks.org/pages/docs/setup_guides/virtual_spr The TUI (option A4 second sub-case) configures Network, WiFi, Admin Password, PLUS token, Advanced Settings, and Virtual SPR mode. It is the recommended path for headless installs. ---------------------------------------------------------------------- 5. REST API — overview ---------------------------------------------------------------------- Base URL on the device: `http://spr.local` or `http://192.168.2.1`. HTTPS is configurable. Auth schemes (any one): - HTTP Basic with a user from `configs/auth/auth_users.json` - Bearer token from `configs/auth/auth_tokens.json` (managed via `/tokens`) - JWT (issued via `/otp_validate` after TOTP) for OTP-gated privileged operations The full reference is rendered from the OpenAPI spec at: https://www.supernetworks.org/pages/api/0 OpenAPI source of truth (canonical type definitions for every request/response — fetch this when you need a schema not inlined here): https://github.com/spr-networks/super/blob/main/api/spr-openapi.yaml Raw URL for direct fetching: https://raw.githubusercontent.com/spr-networks/super/main/api/spr-openapi.yaml LLM tip: this file inlines the most common schemas (Device, ForwardingRule, FirewallBlock, FirewallBlockForward, ServicePort, Endpoint, MulticastPort, CustomInterfaceRule, ICMPOptions). For anything else — DHCPConfig, HostapdConfig, DNSSettings, OTPAuth, AlertSetting, PluginEntry, MeshNode, etc. — read the OpenAPI YAML above. Every endpoint's request/response is grounded in `components/schemas/` in that file. Tag groups in the API: - device, group, policy, psk — device identity, grouping, per-device policy, WiFi PSKs - traffic, dnsblock, dnslog — usage, DNS blocking, DNS query log - network, link, uplink — interface config, WAN/uplink settings - firewall — forward/block rules, service ports, endpoints, ICMP, multicast - hostapd — per-radio WiFi config and channel control - wireguard, dyndns, lookup — VPN, dyndns, helper lookups - tokens, otp — API tokens, OTP enrollment / validation - alerts, releases — alerts inbox, version & update channel - plugins, plus — plugin extensions and PLUS license features - mesh-node, mesh-common, mesh-parent — mesh networking - unix_wifid, unix_dhcpd, unix_wireguard, unix_superd, tinysubnets — internal-only sockets Selected endpoints (most useful for an LLM helping a user): Devices & PSKs GET /devices list devices (map keyed by MAC) PUT /device?identity= create or update a device PUT /device?identity=pending create a wildcard PSK for the next station DELETE /device?identity= remove a device GET /pendingPSK PSKs waiting to be claimed by a station POST /reloadPSKFiles regenerate hostapd PSK files (after bulk edits) Groups GET/PUT/DELETE /groups named device groupings (e.g. "cameras", "tvs") Firewall GET/PUT /firewall/config full firewall config dump PUT/DELETE /firewall/forward port-forward / DNAT rules PUT/DELETE /firewall/block drop rules (input chain) PUT/DELETE /firewall/block_forward drop rules in the forward chain PUT/DELETE /firewall/service_port expose a TCP service on the router itself PUT/DELETE /firewall/endpoint named (host, domain, port) tuples for policy PUT/DELETE /firewall/multicast multicast port policy PUT/DELETE /firewall/icmp ICMP policy (PingLan / PingWan) PUT/DELETE /firewall/custom_interface attach Policies/Groups to a non-WiFi interface Network / Uplink GET/PUT /interfacesConfiguration interface roles GET/PUT /uplink/wifi /uplink/ppp /uplink/ip GET/PUT /link/config POST /link/vlan/{interface}/{state} GET /ip/addr /ip/interfaces /arp /interfaces GET/PUT /subnetConfig GET/PUT /dnsSettings /multicastSettings Hostapd / WiFi GET /hostapd/{interface}/status GET /hostapd/{interface}/all_stations GET/PUT /hostapd/{interface}/config POST /hostapd/{interface}/setChannel POST /hostapd/{interface}/enable POST /hostapd/{interface}/disable POST /hostapd/{interface}/resetConfiguration POST /hostapd/{interface}/enableExtraBSS POST /hostapd/restart GET /iw/list /iw/dev Traffic & logs GET /iptraffic /traffic_history GET /logs /alerts POST /alerts/{index} System GET /status /features /version /release /releaseChannels /releasesAvailable POST /update run an update POST /speedtest POST /ping/{interface}/{address} icmp ping POST /ping/{interface}/{address}/udp udp probe PLUS / extensions / plugins GET/PUT/POST /plugins POST /plugin/install_user_url GET /plugin/custom_compose_paths PUT /plugin/ui_session authenticated, short-lived plugin UI capability POST /startPlusExtension /stopPlusExtension GET/PUT /plusToken Auth POST /otp_register /otp_validate /otp_status /otp_jwt_test GET/PUT/DELETE /tokens ---------------------------------------------------------------------- 6. Working with devices ---------------------------------------------------------------------- A "device" in SPR is identified by MAC (for WiFi/wired) or Wireguard public key (for VPN). Each device has: Name — human label MAC — hardware address ("11:22:33:44:55:66") WGPubKey — Wireguard public key (instead of MAC, for VPN devices) VLANTag — assigned VLAN tag for wired clients RecentIP — most-recent observed IP PSKEntry — { "Type": "sae"|"wpa", "Psk": "passphrase" } - "sae" = WPA3 - "wpa" = WPA2 - if Type is set and Psk is empty, SPR generates a Psk and returns it Policies — connectivity grants from a known set: - "wan" : may reach the internet (forwarding) - "dns" : may use the SPR DNS resolver - "lan" : this device may initiate connections to all LAN devices; it does not grant other devices access to this device - "lan_upstream" : this device may reach RFC1918 private addresses upstream of SPR; blocked by default for security - "api" : may call the SPR API - "disabled" : explicitly off Groups — user-defined sets that let listed devices reach each other (e.g. ["cameras"], ["tvs", "media"]) DeviceTags — arbitrary labels used by firewall and DNS rules CRITICAL: "Policies" controls reachability buckets (WAN/DNS/LAN/etc). "Groups" controls which other devices a device can talk to. They are not the same field. Do not put "wan" or "dns" in Groups. Auth setup once: SPR=http://spr.local AUTH='-u admin:YOUR_PASSWORD' # or: AUTH='-H "Authorization: Bearer "' List all known devices (returns a map keyed by MAC): curl $AUTH $SPR/devices Get one device by MAC: curl $AUTH "$SPR/device?identity=11:22:33:44:55:66" Add or update a device with a chosen WPA3 passphrase and let it reach the internet + DNS: curl -X PUT $AUTH "$SPR/device?identity=11:22:33:44:55:66" \ -H 'Content-Type: application/json' \ -d '{ "Name": "laptop", "MAC": "11:22:33:44:55:66", "PSKEntry": { "Type": "sae", "Psk": "correcthorsebatterystaple" }, "Policies": ["wan", "dns"], "Groups": [], "DeviceTags": ["trusted"] }' Add a device but let SPR generate the passphrase (omit Psk; the response contains the generated value): curl -X PUT $AUTH "$SPR/device?identity=11:22:33:44:55:66" \ -H 'Content-Type: application/json' \ -d '{ "Name": "iot-bulb", "PSKEntry": { "Type": "sae" }, "Policies": ["wan", "dns"] }' Quickest "let the next phone that joins claim a fresh PSK" (wildcard / pending): curl -X PUT $AUTH "$SPR/device?identity=pending" \ -H 'Content-Type: application/json' \ -d '{ "PSKEntry": { "Type": "sae" }, "Policies": ["wan", "dns"] }' # Then list pending claims: curl $AUTH $SPR/pendingPSK Place two devices into the same intra-LAN group ("cameras") so they can reach each other (and put the NVR in the same group + grant it WAN): # The cameras themselves: in-group, no internet curl -X PUT $AUTH "$SPR/device?identity=AA:AA:AA:AA:AA:01" \ -d '{"Name":"cam-front","PSKEntry":{"Type":"sae"},"Policies":["dns"],"Groups":["cameras"]}' curl -X PUT $AUTH "$SPR/device?identity=AA:AA:AA:AA:AA:02" \ -d '{"Name":"cam-back","PSKEntry":{"Type":"sae"},"Policies":["dns"],"Groups":["cameras"]}' # The NVR: in the group AND with WAN access curl -X PUT $AUTH "$SPR/device?identity=BB:BB:BB:BB:BB:01" \ -d '{"Name":"nvr","PSKEntry":{"Type":"sae"},"Policies":["wan","dns"],"Groups":["cameras"]}' Remove a device: curl -X DELETE $AUTH "$SPR/device?identity=11:22:33:44:55:66" After bulk PSK edits made out-of-band, force hostapd to reload its PSK files: curl -X POST $AUTH $SPR/reloadPSKFiles Inspect what stations are actually associated to a radio (independent of the device list): curl $AUTH $SPR/hostapd/wlan1/all_stations User-facing guide: https://www.supernetworks.org/pages/docs/guides/device ---------------------------------------------------------------------- 7. Working with the firewall ---------------------------------------------------------------------- SPR's firewall is default-deny. Rules are stored as JSON and compiled into nftables. The shape of the full config is roughly: { "ForwardingRules": [ ForwardingRule, ... ], // port-forward / DNAT "BlockRules": [ FirewallBlock, ... ], // drop on input "ForwardingBlockRules": [ FirewallBlockForward, ... ], // drop on forward "ServicePorts": [ ServicePort, ... ], // ports the router itself exposes "Endpoints": [ Endpoint, ... ], // named (host/domain, port) tuples "MulticastPorts": [ MulticastPort, ... ], "ICMP": { "PingLan": bool, "PingWan": bool }, "CustomInterfaceRules": [ CustomInterfaceRule, ... ] } Read it: curl $AUTH $SPR/firewall/config Schema reference for individual rules: ForwardingRule (port-forward / DNAT, used via PUT/DELETE /firewall/forward): { "Protocol": "tcp"|"udp", "SrcIP": "0.0.0.0/0", // who is allowed to hit it (CIDR) "SrcPort": "8080", // public port on the router (or "any") "DstIP": "192.168.2.50", // internal device IP "DstPort": "8080" } // port on internal device (or "any") FirewallBlock (PUT/DELETE /firewall/block — drop on input): { "Protocol": "tcp"|"udp", "SrcIP": "1.2.3.4", "DstIP": "0.0.0.0/0" } FirewallBlockForward (PUT/DELETE /firewall/block_forward — drop on forward): { "Protocol": "tcp"|"udp", "SrcIP": "192.168.2.50", "DstIP": "0.0.0.0/0", "DstPort": "0-65535" } ServicePort (PUT/DELETE /firewall/service_port — expose a TCP service on the router): { "Protocol": "tcp", "Port": "22", "UpstreamEnabled": false } // true = also reachable from WAN Endpoint (PUT/DELETE /firewall/endpoint — named tuple referenced by other policy): { "RuleName": "github-api", "Disabled": false, "Protocol": "tcp", "IP": "1.2.3.4", "Domain": "api.github.com", "Port": "443", "Tags": ["dev"] } MulticastPort (PUT/DELETE /firewall/multicast): { "Port": "5353", "Upstream": false } CustomInterfaceRule (PUT/DELETE /firewall/custom_interface): { "Interface": "wlan1", "SrcIP": "172.16.0.0/16", "RouteDst": "172.12.0.2", "Policies": ["api","wan","dns","lan_upstream"], "Groups": ["cameras"], "Tags": ["private"] } ICMPOptions (PUT /firewall/icmp): { "PingLan": true, "PingWan": false } Concrete recipes: # Expose port 8080 on the router and forward it to an internal web server curl -X PUT $AUTH $SPR/firewall/forward \ -H 'Content-Type: application/json' \ -d '{"Protocol":"tcp","SrcIP":"0.0.0.0/0","SrcPort":"8080","DstIP":"192.168.2.50","DstPort":"8080"}' # Stop a noisy IoT device from reaching the internet curl -X PUT $AUTH $SPR/firewall/block_forward \ -H 'Content-Type: application/json' \ -d '{"Protocol":"tcp","SrcIP":"192.168.2.77","DstIP":"0.0.0.0/0","DstPort":"0-65535"}' curl -X PUT $AUTH $SPR/firewall/block_forward \ -H 'Content-Type: application/json' \ -d '{"Protocol":"udp","SrcIP":"192.168.2.77","DstIP":"0.0.0.0/0","DstPort":"0-65535"}' # Open SSH on the router from LAN only (UpstreamEnabled=false) curl -X PUT $AUTH $SPR/firewall/service_port \ -H 'Content-Type: application/json' \ -d '{"Protocol":"tcp","Port":"22","UpstreamEnabled":false}' # Allow ping from LAN, deny from WAN curl -X PUT $AUTH $SPR/firewall/icmp \ -H 'Content-Type: application/json' \ -d '{"PingLan":true,"PingWan":false}' # Remove a rule: send the same JSON to DELETE curl -X DELETE $AUTH $SPR/firewall/forward \ -H 'Content-Type: application/json' \ -d '{"Protocol":"tcp","SrcIP":"0.0.0.0/0","SrcPort":"8080","DstIP":"192.168.2.50","DstPort":"8080"}' User-facing guide: https://www.supernetworks.org/pages/docs/guides/firewall ---------------------------------------------------------------------- 8. Other useful endpoints ---------------------------------------------------------------------- # Hostapd: list stations / config / restart curl $AUTH $SPR/hostapd/wlan1/status curl $AUTH $SPR/hostapd/wlan1/all_stations curl $AUTH $SPR/hostapd/wlan1/config curl -X POST $AUTH $SPR/hostapd/restart # Traffic and DNS visibility curl $AUTH $SPR/iptraffic curl $AUTH $SPR/traffic_history curl $AUTH $SPR/logs curl $AUTH $SPR/alerts # Version & updates curl $AUTH $SPR/version curl $AUTH $SPR/release curl $AUTH $SPR/releaseChannels curl $AUTH $SPR/releasesAvailable curl -X POST $AUTH $SPR/update # API tokens (for non-interactive use) curl $AUTH $SPR/tokens # list curl -X PUT $AUTH $SPR/tokens \ -H 'Content-Type: application/json' \ -d '{"Name":"automation","ScopedPaths":["/devices","/firewall/config"]}' # OTP / TOTP for privileged operations curl -X POST $AUTH $SPR/otp_register curl -X POST $AUTH $SPR/otp_validate -d '{"Code":"123456"}' ---------------------------------------------------------------------- 9. Vocabulary cheat sheet ---------------------------------------------------------------------- - "Per-device VLAN" = one Linux AP/VLAN virtual interface per WiFi station, e.g. wlan1.0, wlan1.1, ... - "PSK" = pre-shared key = the WiFi passphrase. SPR issues one per device. - "Policy" = a built-in connectivity grant such as `wan`, `dns`, `lan`, or `lan_upstream`. - "Group" = a user-defined set whose listed devices may communicate with one another. - "Tag" = arbitrary label, used to target firewall rules and DNS settings. - "Endpoint" = a named (host, port, protocol) tuple used in firewall rules. - "PLUS" = SPR's licensed extension layer (scheduled rules, domain-based policy, mesh, etc.). Activated with a PLUS token. - "Plugin UI session" = a short-lived bearer capability issued through an authenticated parent UI and restricted to one plugin's `/plugins/` route. - "Install token" = an optional backend credential written under `/configs/plugins/` and restricted by `ScopedPaths`; it is not a browser credential. - "Virtual SPR" = SPR running as a VPN-only gateway, no WiFi radio. - "Mesh" = SPR-to-SPR uplink for extending a network with PLUS. ---------------------------------------------------------------------- 10. Where to send users when they need more ---------------------------------------------------------------------- - Documentation: https://www.supernetworks.org/pages/docs/intro - Setup guide: https://www.supernetworks.org/pages/docs/setup_guides/setup_run_spr - Install walk: https://www.supernetworks.org/pages/docs/setup_guides/install - API reference: https://www.supernetworks.org/pages/api/0 - Plugin guide: https://www.supernetworks.org/pages/docs/guides/plugins - Plugin dev: https://www.supernetworks.org/pages/docs/development/plugin-dev - Articles: https://www.supernetworks.org/pages/articles - Blog: https://www.supernetworks.org/pages/blog - Source: https://github.com/spr-networks/super - Releases: https://github.com/spr-networks/super/releases - Discord: https://discord.gg/EUjTKJPPAX - Buy hardware: https://www.supernetworks.org/compute-board.html If a user is stuck on initial setup and can't reach the web UI, recommend the TUI: ssh ubuntu@192.168.2.1 # or: ssh ubuntu@spr.local # default password: ubuntu sudo /home/spr/super/base/scripts/spr-setup-tui.sh