Architecture
swsrs is a relay, not a VPN. Its single job: get bytes from one authenticated peer to another, regardless of what those bytes mean.
Surfaces
Two distinct HTTP surfaces, deliberately different:
| Surface | Path | Auth | Purpose |
|---|---|---|---|
| Admin API | /admin/sessions | OIDC JWT (JWKS auto-discovery, scope-gated) | Create / list / delete sessions |
| Data plane | /relay/{id} | Opaque per-slot token (minted by admin) | Two peers connect; opaque bytes flow between them |
| Discovery | /.well-known/swsrs-config | Public | Lets clients run their own OAuth flow without hard-coding the IdP |
| Health | /healthz | Public | Liveness probe |
This split is load-bearing. See Authentication for the why.
Session lifecycle
A session is a two-slot rendezvous:
- States:
pending→half_open→open→closed. - Each session has an
initiatorand aresponderslot. Names express caller intent; the wire is symmetric. - TTL (default 1h) bounds session lifetime regardless of activity.
peer_wait(default 2m) bounds how long the first-arriving peer waits for its counterpart. When it expires that peer is disconnected, but the session stayspendingand both tokens remain valid until the TTL.- When a session closes (TTL or
DELETE /admin/sessions/{id}), connected peers get a close frame with status 1001 (going away). - A periodic reaper sweeps expired sessions.
- A second connection to a slot that is already connected gets HTTP 409.
The slot model means one TCP/UDP connection per session. For N concurrent tunnels, mint N sessions. Multiplexing is a client-side concern — it would mean inspecting frames, which the server doesn't do.
Protocol-agnostic relay
The server treats every session as a single stream of opaque WebSocket binary frames. It does not parse, inspect, or modify payloads:
- Adding TCP semantics? Client-side (
swsrs tcp-listen/tcp-dial). - Adding UDP semantics? Client-side (one WS frame == one datagram).
- Adding gRPC, SSH, HTTP, TLS? The Go SDK gives you a
net.Conn; drop it into any library that takes one.
This is why the binary stays so small: the relay's threat model is trivial (it sees ciphertext-equivalent blobs), and adding new protocols never requires a server release.
Backpressure
The forwarding loop is synchronous per direction and streams each message through in chunks instead of reading it whole, so memory per connection stays bounded whatever the message size. A slow peer creates backpressure on the fast peer (the relay won't buffer unbounded). If a peer's WebSocket write blocks, both sides eventually close. No in-memory queue per-session means no OOM surprises on a t4g.nano.
What's NOT built in
Worth being explicit about scope:
- No transparent reconnect within the peer-wait grace window — the drop surfaces as a read/write error and callers redial with the same token.
- No server-side WS pings — the relay answers pings (including while a peer waits for its counterpart) but never sends them. The Go SDK pings; browsers can't.
- No transparent multi-stream multiplexing — one TCP/UDP per session.
- No per-user identity on the data plane — slot tokens are opaque, not JWTs. If you need an audit trail of who connected, your control plane records it when it hands out tokens.
- Single port for admin + data plane. A surge of relay traffic could starve admin requests; if that's a real concern, run two listeners behind a reverse proxy.
These are deliberate; they keep the surface auditable and the binary tiny. Add layers above the relay where they belong.