Architecture¶
Jumo is a pair of OpenWrt routers. The home router sits next to the customer's ISP box. The travel router goes with the customer. A laptop plugged into the travel router (cable or Wi-Fi) is on the home network: it gets its address from the home ISP box and goes out to the internet from home.
flowchart LR
laptop[Company laptop] -- "LAN / Wi-Fi" --> travel[Travel router]
travel <-- "WireGuard x2 paths + VXLAN (L2)" --> home[Home router]
home -- "uplink, bridged" --> isp[ISP box] --> internet((Internet))
travel -. "Connect RPC + WebSocket" .-> cp["Control plane<br/>Cloudflare Workers"]
home -. "Connect RPC + WebSocket" .-> cp
staff["Staff: admin, debug console, kit"] --> cp
Principles¶
- We don't run the data plane. Customer traffic goes directly between the two routers over WireGuard. No account, no licence, nothing of ours in the path. If the control plane is down, a connected pair keeps working. (A relay is planned as a last resort for connections where neither router is reachable; it would carry traffic and is a product decision.)
- A router must never need a site visit. Every config change is confirm-or-rollback; a factory reset re-enrolls on its own; staff can run commands remotely through the debug console.
- One schema.
platform/protois the single source of truth for the Go agent, the Worker and the admin. Nothing is synced by hand. - Accessible and fast. The admin is a small client-side app: pages show at once, only the data waits. It targets WCAG 2.2 AA with a screen-reader focus.
Repositories (GitLab group jumo-router)¶
| Repo | What | Stack |
|---|---|---|
platform/proto |
API contract; buf lint and buf breaking in CI |
Protobuf, Buf |
platform/control-plane |
Device and admin APIs, realtime rooms, staff auth | Workers, Hono, Connect v2, D1, Durable Objects, Better Auth |
platform/web |
Staff admin | SvelteKit 2, Svelte 5, native CSS |
edge/agent |
edge-agent on the routers (ea), technician kit image |
Go, connect-go, coder/websocket, cobra |
edge/firmware |
OpenWrt images, lab and PoC scripts | ImageBuilder, QEMU, shell |
infra/ci-templates |
Shared GitLab CI | GitLab CI |
docs |
This site, docs.jumorouter.com |
MkDocs Material |
Consumers generate code from ../proto and commit it, so each repo builds on
its own. See Deployment for environments and CI.
How a pair comes up¶
- Flash. Staff flash a Jumo image: OpenWrt + agent + a fleet enroll code + the control plane URL, all in the read-only part of the image.
- Enroll. On first boot the agent sends code + MAC; the control plane registers the router (unknown MAC) or gives back its identity (known MAC, e.g. after a factory reset). The agent creates its WireGuard key; only the public key leaves the router.
- Pair. Staff pair a home and a travel router in the admin.
- Apply. Each router renders its tunnel config (two WireGuard paths, VXLAN, bridge) and applies it through the guard: snapshot, apply, keep only if the control plane is still reachable within 90 s.
- Ladder. The home router opens its endpoints on the ISP box (IPv6 pinhole, IPv4 port mapping), one per path port, and reports them. The control plane pushes them to the travel router over its realtime link.
- Dial. The travel router dials each path with the rungs in a different order (path 1 IPv6 first, path 2 IPv4 first), so the paths cross the internet differently.
- Carry. The path watcher routes VXLAN over a healthy path and fails over in about 3 seconds. The laptop is on the home network.
Details: Agent, WireGuard PoC.
Data plane¶
- WireGuard + VXLAN, in the kernel. Fast on cheap MIPS routers, a few KB of flash. VXLAN between fixed loopback addresses (10.99.1.1 ↔ 10.99.1.2) carries the home LAN; each path is its own /30 (10.99.0.0/30, 10.99.0.4/30) and port (51820, 51821).
- Only one side needs to be reachable. Today the home router; the travel side as the reachable one is planned.
- Connection ladder, best first. Done: IPv6 + pinhole (UPnP IGD v2), IPv4 + port mapping (UPnP), and on a shared-IPv4 box (Free) a port the box picks inside the subscriber's range (AddAnyPortMapping), kept across renewals. Planned: NAT-PMP/PCP, the travel side reachable, TCP 443 fallback, hole punching (STUN), relay.
- Two paths always up, renewed at different times; switching is a route change. Measured: 146 s outage with one path, about 3 s with two.
Why not ZeroTier or Tailscale: see the decision log.
Control plane¶
- Connect on both ends. The Go agent speaks binary protobuf; the Worker serves the same services through Connect v2's fetch adapters mounted in Hono; the admin calls them server-side.
- Realtime: one Durable Object per device, hibernating WebSocket, protobuf frames. Used to push "state changed" (sync now) and to run debug commands. Polling stays the source of truth.
- D1 for devices, enroll codes, pairs, command runs and staff accounts.
- Staff auth: Better Auth (email + password, sign-up closed, PBKDF2 via Web Crypto). The admin API also accepts a bearer key (CI, jumoctl).
Admin¶
- SvelteKit as a client-side app (
ssr = false). Pages render at once and fetch with Connect from/api; data is kept between pages, so a page opened again shows its last data and refreshes in the background. - One origin: the admin worker serves the app and proxies
/api(staff services only) and/auth(sign-in, sign-out, password) to the control plane over a service binding. Same cookie, no CORS; writes must carry the admin's origin. - Staff sessions: the control plane checks the signed Better Auth cookie and
reads session + account in one D1 query (Better Auth's own
getSessiontook 0.4-0.8 s on Workers). The control plane runs with Smart Placement, next to D1. - Tables on wide screens, cards on phones; only one is in the accessibility tree at a time.
- Pages: devices (register, label), device (facts, debug console), pairs (tunnel state), enroll codes (fleet codes, revoke).
Identity and enrollment¶
- Fleet codes (generic firmware, revocable, optional cap) and per-device codes. Codes are 80-bit Crockford base32, stored hashed.
- A router is identified by MAC. Re-enrolling replaces the token and closes the old realtime socket. Admin flags routers enrolled more than 3 times.
- Known risk: a fleet code can be extracted from firmware and a MAC spoofed. A per-device secret in a flash area that survives reset closes this (board-specific, later).
Code style¶
- TypeScript:
strictplusexactOptionalPropertyTypes,noUncheckedIndexedAccess,noPropertyAccessFromIndexSignature; typescript-eslintstrictTypeChecked+stylisticTypeChecked+ unicorn, zero warnings, noas. Svelte:svelte-check --fail-on-warnings(a11y warnings fail the build). - Go: golangci-lint v2 with
default: all; the few disabled linters carry a reason in.golangci.yml. - Protobuf:
buf lintSTANDARD + COMMENTS,buf breakingFILE.