How It Works

One config file in.
A parallel internet out.

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.

The Deploy Stack

Five layers, each one idempotent.

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.

Layergspcli deploy k8s

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.

Layergspcli deploy 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.

Layergspcli deploy customers

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.

Layergspcli deploy wanemu

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.

Layergspcli deploy ui

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.

Declarative C2

The battlespace is a file.

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.

config.yaml
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
WAN Emulation

A bump in the wire the routers can't see.

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.

ROUTER A PE · ISP pacific ROUTER B PE · ISP edge-site WAN EMULATOR in-kernel bridge + netem per-direction shaping profile: leo-starlink seg-A (L2) seg-B (L2) eBGP adjacency — direct, unchanged, stays established one shared /29 · routers ARP each other's real MACs · emulator invisible at L2 and L3

Every impairment class

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.

Live, scriptable control

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.

Fail-open, observable

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.

Built-in link profiles

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.

ProfileBandwidthLatency (1-way)LossBehavior
Static terrestrial
terrestrial-fiber1 gbit5 msstatic clean long-haul baseline
terrestrial-congested50 mbit40 ms ±100.5%static oversubscribed link, deep-queue bufferbloat
idlestatic emulation off — clean pass-through
Static satellite
geo-satcom20 mbit280 ms0.3%satcom classic geostationary lag
meo-satcom50 mbit70 ms0.2%satcom mid-orbit, lower delay than GEO
o3b-meo100 mbit70 ms0.2%satcom high-throughput equatorial MEO
wgs-ka-maritime20 mbit280 ms0.5% burstysatcom military Ka afloat, correlated RF loss
muos-narrowband128 kbit280 ms1%satcom UHF narrowband — near-DTN
Dynamic satellite
leo-starlink150 mbit25 ms ±100.5%dynamic +40 ms / 8% loss spike every ~15 s at handover
leo-degraded80 mbit40 ms ±203%dynamic 22% correlated loss bursts — obstruction / weather
rain-fade30 → 6 mbit280 ms2 → 8%dynamic ~90 s storm: fade in, dwell, recover, clear
wgs-ka-blockage20 mbit280 mshard dropdynamic 8 s outage every 45 s — mast/superstructure blockage
starlink-maritime120 mbit45 ms0.5%dynamic handover + motion-mispointing fade at sea
acm-ladder40 mbit stepped280 msfloor 5%dynamic modcod step-down through modem tiers under fade
gilbert-elliott20 mbit280 ms0.2 → 15%dynamic good/bad-regime burst loss (2-state Markov)
sea-state20 mbit280 ms0.3%dynamic swell mispointing + range-rate delay wobble
emcon20 mbit280 msblackoutdynamic scheduled 30 s radio-silence outages (EMCON / River City)
Dynamic terrestrial
busy-hour100 → 25 mbit20 ms0.5%dynamic diurnal congestion — buffers deepen at peak load
link-flap50 mbit25 msbrownoutdynamic ~2 s outages on flaky backhaul
mobile-lte40 mbit40 ms ±100.3%dynamic cell-load swings + periodic tower handovers
congestion-collapse100 → 30 mbit20 ms8%dynamic buffers fill, throughput collapses, then recovers
Your own
custombase + dynamics, your valuesapi author in YAML, validate, load live — no rebuild
Customer Ingress

Three ways onto the wire.

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.

ModeYou have…Device setupIsolation
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

soho

Your whole enclave shares one public identity — home-broadband shape. Outbound from every device, PAT behind the CPE.

cloud

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.

routed

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.

Airgap Pipeline

Nothing phones home. Ever.

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.

connected build host
$ 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
Command & Control

The whole internet on one pane of glass.

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.

3D globe & 2D topology

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.

WANEMU station

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.

Customer operations

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.

API-first

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.

Want the deep dive?

Architecture briefings and live walk-throughs available for evaluation teams.