A pure-Go, CGO_ENABLED=0 remote-browser service. It renders web pages
server-side with the pure-Go go-webengine/engine
and streams frames (plus a hyperlink hit-map) to a thin client, forwarding the
client's clicks, scrolls and keys back as navigation. No Chromium, no cgo, no
host web view.
The wire protocol is gRPC carried over
grpc-transports/websocket: a
single bidirectional Session stream per tab. Because that transport ships a
zero-dependency syscall/js client, the same client compiles to
GOOS=js/GOARCH=wasm and runs in the browser with no sidecar proxy — the full
gRPC feature set (client- and bidi-streaming), which plain grpc-web cannot do.
Because rendering happens on the server:
- any site can be shown — including pages that set
X-Frame-Options: DENYor a restrictiveframe-ancestorsCSP, which an<iframe>embed could never load; - the client page can stay under
COEP: require-corp(needed forSharedArrayBuffer) — a WebSocket is exempt from COEP/CORS.
It is the server half of the wasmdesk
clients/browser in-desktop browser. See wasmclient/ for a
worked GOOS=js/wasm client.
$ go run ./cmd/browserproxy -addr :8090
browserproxy: listening on :8090 (gRPC/WebSocket path /ws)Then dial the browserproxy.v1.Browser gRPC service over the WebSocket
transport at ws://localhost:8090/ws (native or GOOS=js/wasm):
opt, _ := wstransport.DialOption("ws://localhost:8090/ws", wstransport.ClientConfig{})
cc, _ := grpc.NewClient("passthrough:///browserproxy",
grpc.WithTransportCredentials(insecure.NewCredentials()), opt)
stream, _ := browserpb.NewBrowserClient(cc).Session(ctx)Flags:
| flag | default | meaning |
|---|---|---|
-addr |
:8090 |
listen address |
-origins |
"" (any) |
comma-separated WebSocket Origin allowlist (* = any) |
-max-concurrent |
4 |
global cap on concurrent page renders |
-min-nav-interval |
250ms |
per-session minimum time between navigations |
-width / -height |
1024 / 768 |
default viewport size |
-render-timeout |
35s |
per-navigation render timeout |
One gRPC bidirectional Session stream per tab (proto/browser.proto). Client →
server ClientMsg: navigate, click, scroll, key, resize, back,
forward. Server → client ServerMsg: frame (png,w,h,offset_y — raw
PNG bytes, no base64), state (url,title,loading,can_back,
can_forward), error. Full spec: docs/protocol.md.
Every fetch — the top navigation and every subresource, redirect and DNS-rebinding attempt — passes an SSRF guard before the socket connects:
- non-
http(s)schemes rejected (file:,data:,javascript:, …); - cloud metadata
169.254.169.254, loopback, RFC1918 private, link-local, IPv6 ULA (fc00::/7), CGNAT (100.64.0.0/10), unspecified and multicast addresses blocked at dial time (post-DNS); - internal namespaces (
localhost,*.internal,*.local,*.lan,*.home.arpa) blocked by name.
Plus a per-session navigation rate limit, a global concurrent-render cap, and a
configurable WebSocket Origin allowlist.
$ go test -short ./... # unit tests only (no network); root package at 100%
$ go test ./... # also runs the live example.com integration testThree layers of end-to-end proof, all over the real gRPC-over-WebSocket wire:
TestIntegration_StubbedTransport— a real gRPC client drives a stub-rendered server (hermetic, no network);TestWasmClientE2E— compileswasmclient/tojs/wasmand runs it under Node against a live server, asserting a full bidirectional Session — proof a pure-Go browser client actually runs;TestIntegration_ExampleCom(non--short) — the same client and transport drive a real engine-backed server againsthttps://example.com, asserting a non-blank frame and the page title.
BSD-3-Clause — see LICENSE.
