GSP is a single Go binary — gspcli — that turns a set of Linux hosts
into a complete emulated internet, over SSH, fully airgapped. Everything is
declared in one config.yaml; everything deploys as independently
reconcilable layers.
Run gspcli deploy for all of them in order, or any single layer by name. Re-running a layer with nothing changed does nothing — by design, provably.
The foundation. Provisions an RKE2 Kubernetes cluster over SSH on your hosts — VMs or bare metal, any x86-64 Linux. Cilium + Multus + Kube-OVN networking, installed entirely from the airgap package. No node ever needs internet.
The internet itself. Namespaced VyOS routers form each ISP: multi-area OSPF inside, iBGP route reflection on the cores, eBGP peering between ISPs, border-only aggregation and provider/customer transit — the same policy discipline real carriers run. Hop counts, TTL decrements, and traceroute paths all read true.
The on-ramps. Per-ISP customer ingress: aggregation routers, emulated DNS, and one CPE router per customer on standalone gateway hosts. Real devices get public identities in the emulated address space, behind the same public/private boundary a real ISP draws — no hypervisor tricks, no promiscuous mode.
The weather. Transparent WAN emulators insert on the inter-ISP links you choose, applying SATCOM, congestion, and loss profiles without the routers noticing. The link's L3 view is untouched — adjacencies stay up while conditions degrade.
The picture. The GSP UI serves a live 3D globe and 2D topology of everything deployed, WAN-emulator control, and customer management — all backed by a REST API, so every workflow is scriptable.
Nodes, ISPs, peering, customers, impairments — one YAML file declares all of
it. gspcli plan shows exactly what a deploy would change before it
touches anything.
The live system is the source of truth. gspcli observes, diffs, and applies: transfers are hash-gated, configs rewritten only on real drift, services restarted only when needed. Manual tampering is converged away on the next run.
Deterministic, too: the same config renders byte-identical router configurations every time. A failing test can be re-run under the exact same internet.
nodes: - {ip: 10.20.1.11, type: all} - {ip: 10.20.1.12, type: all} - {ip: 10.20.1.13, type: agent} isps: - name: pacific # cores, providers, PEs rendered per ISP - name: conus - name: edge-site standalone: true links: [pacific] # single-homed customer of pacific wanemu: - name: pac-edge links: [pacific, edge-site] profile: leo-starlink # handover spike every ~15 s customers: - name: afloat-02 isp: pacific type: cloud # each device owns a public identity gateway: gsp-gateway-1
The emulator is a transparent Layer-2 device between two routers. They keep one subnet, see each other's real MAC addresses, and hold their BGP adjacency directly — while every packet between them crosses the impairment engine.
Bandwidth caps, latency, jitter (with distribution shape), random loss and loss correlation for realistic bursts, bit corruption, duplication, reordering, and queue-depth control for bufferbloat. Each direction shaped independently — real links are lopsided.
Switch profiles, dial parameters, or kill a link mid-test — REST API, no restart, no disruption to basic connectivity. Drive it by hand from the UI or by automation from a test harness.
If the control plane dies, traffic keeps flowing unshaped (fail-closed configurable). SNMP reports the impairment actually applied right now — your dashboards never see stale intent.
Two dozen presets ship in the box — static links plus dynamic ones that vary on a deterministic schedule. Every one is live and annotated on the WAN Profiles page. Any profile is a static base plus a stack of time-varying dynamics, so you can compose your own.
| Profile | Bandwidth | Latency (1-way) | Loss | Behavior |
|---|---|---|---|---|
| Static terrestrial | ||||
terrestrial-fiber | 1 gbit | 5 ms | — | static clean long-haul baseline |
terrestrial-congested | 50 mbit | 40 ms ±10 | 0.5% | static oversubscribed link, deep-queue bufferbloat |
idle | — | — | — | static emulation off — clean pass-through |
| Static satellite | ||||
geo-satcom | 20 mbit | 280 ms | 0.3% | satcom classic geostationary lag |
meo-satcom | 50 mbit | 70 ms | 0.2% | satcom mid-orbit, lower delay than GEO |
o3b-meo | 100 mbit | 70 ms | 0.2% | satcom high-throughput equatorial MEO |
wgs-ka-maritime | 20 mbit | 280 ms | 0.5% bursty | satcom military Ka afloat, correlated RF loss |
muos-narrowband | 128 kbit | 280 ms | 1% | satcom UHF narrowband — near-DTN |
| Dynamic satellite | ||||
leo-starlink | 150 mbit | 25 ms ±10 | 0.5% | dynamic +40 ms / 8% loss spike every ~15 s at handover |
leo-degraded | 80 mbit | 40 ms ±20 | 3% | dynamic 22% correlated loss bursts — obstruction / weather |
rain-fade | 30 → 6 mbit | 280 ms | 2 → 8% | dynamic ~90 s storm: fade in, dwell, recover, clear |
wgs-ka-blockage | 20 mbit | 280 ms | hard drop | dynamic 8 s outage every 45 s — mast/superstructure blockage |
starlink-maritime | 120 mbit | 45 ms | 0.5% | dynamic handover + motion-mispointing fade at sea |
acm-ladder | 40 mbit stepped | 280 ms | floor 5% | dynamic modcod step-down through modem tiers under fade |
gilbert-elliott | 20 mbit | 280 ms | 0.2 → 15% | dynamic good/bad-regime burst loss (2-state Markov) |
sea-state | 20 mbit | 280 ms | 0.3% | dynamic swell mispointing + range-rate delay wobble |
emcon | 20 mbit | 280 ms | blackout | dynamic scheduled 30 s radio-silence outages (EMCON / River City) |
| Dynamic terrestrial | ||||
busy-hour | 100 → 25 mbit | 20 ms | 0.5% | dynamic diurnal congestion — buffers deepen at peak load |
link-flap | 50 mbit | 25 ms | brownout | dynamic ~2 s outages on flaky backhaul |
mobile-lte | 40 mbit | 40 ms ±10 | 0.3% | dynamic cell-load swings + periodic tower handovers |
congestion-collapse | 100 → 30 mbit | 20 ms | 8% | dynamic buffers fill, throughput collapses, then recovers |
| Your own | ||||
custom | base + dynamics, your values | api author in YAML, validate, load live — no rebuild | ||
Real hardware joins the emulated internet with a public identity inside it, and its traffic crosses the full ISP mesh and WAN impairments. The public/private addressing boundary sits exactly where a real ISP puts it — pick the on-ramp that matches the situation.
| Mode | You have… | Device setup | Isolation |
|---|---|---|---|
| Flat LAN | devices on the same LAN as a small GSP gateway host | add routes pointing at your customer's CPE — that's all | shared LAN |
| Trunk / VLAN | managed switches or a virtualization platform | put the device in its VLAN — DHCP delivers IP, routes, and emulated DNS | per-customer VLAN |
gspcli connect |
a laptop or VM, anywhere | one command — encrypted tunnel, own public identity, default route untouched | tunnel |
Your whole enclave shares one public identity — home-broadband shape. Outbound from every device, PAT behind the CPE.
Every device owns a dedicated public identity from your carved prefix. Inbound works out of the box — run servers, take connections, capture real peer addresses.
Your prefix is delegated to your router across a transfer /29 — a true PE-CE handoff. Everything behind it is your design; GSP never touches your enclave.
Connected side: gspcli package build fetches
Kubernetes, CNI images, router and emulator containers, and the UI — no Docker
or Helm needed on the build machine — and writes one versioned tarball plus a
manifest with the sha256 of every file. Rebuilds are incremental and
digest-verified.
Disconnected side: point config.yaml at the
package. At deploy, the tarball is pushed once per node and everything installs
from that local extraction. Corrupted or tampered artifacts refuse to deploy.
Nodes need Linux, SSH, and ~20 GB of disk. That's the whole dependency list.
$ gspcli package build fetch rke2 v1.35.5+rke2r2 verified fetch kube-ovn v1.16.2 verified fetch gsp-vyos, gsp-wanemu, gsp-ui verified ✓ gsp-package-v1.35.5-rke2r2.tar.gz ✓ manifest.json (sha256 of every artifact) # sneakernet across the airgap, then: $ gspcli deploy k8s push package → nodes (hash-gated, once) ✓ cluster Ready — zero internet access used
The GSP UI discovers whatever you deployed and renders it live — no separate globe config, no drawing. Deploy a router, it appears; delete it, it's gone.
Routers stand on the Earth as datacenter and ground-station meshes; peering links arc between regions; SATCOM links climb to a satellite at apex, dash speed keyed to configured latency. Click anything for live status, config, and an in-browser SSH terminal.
Every emulator with its live impairment state. Apply profiles, tune per-direction parameters, disable a link mid-exercise — and watch the traffic react on the map.
An ingress map from gateway to CPE to aggregation to PE, live tunnel state from real WireGuard handshakes, per-customer throughput, and copy-paste device onboarding sheets that can never drift from what's deployed.
There is no UI-only logic. Everything the UI does rides a REST API — topology, impairment control, customer lifecycle — so exercises, test harnesses, and range automation drive GSP the same way an operator does.
Architecture briefings and live walk-throughs available for evaluation teams.