2026-05-28 19:08:04 +00:00

webnet

A TypeScript/WebAssembly SDK for running Tailscale inside a browser. It wraps the existing tsconnect Go package from the Tailscale repository, compiles it to WASM, and exposes a typed JavaScript API for joining a tailnet, opening TCP connections, dialing TLS, and listening, all from within a web page.

Inspiration

This is heavily inspired by the WebVM networking stack that levrages Tailscale in the browser. Note that this doesn't lift any code from that project, only ideas.

Another inspiration is the ElysiaJS documentation that seemingly allows running a webserver from the docs directly in the browser (even if it is just a feature of the framework itself and not real networking).

How it works

Tailscale already ships a tsconnect package that compiles the IPN (in-process networking) stack to WASM via GOARCH=wasm. This repo builds on top of that by:

  1. Patching tsconnect: the tailscale submodule tracks a fork on the webnet branch that extends the Go-to-JS bridge (wasm_js.go) to expose lower-level networking primitives: raw TCP/UDP connections, ICMP, TLS dialing, and TCP listening.
  2. @webnet/tsconnect: an npm package (packages/tsconnect) that builds the WASM artifact, ships it alongside a Mozilla CA bundle and wasm_exec.js, and wraps the raw JS bridge in typed TypeScript classes with argument validation to avoid a Go panic that kills the WASM instance.
  3. @webnet/test-app: a Vite dev app (packages/test-app) for manual browser testing. It exposes initIPN, wasmURL, cacertURL, and loadCACerts on window so the full stack can be exercised from the browser console without writing any test code.
  4. @webnet/http: an HTTP/1.1 server library (packages/http) built on top of the RawTransport/RawListener primitives exposed by @webnet/tsconnect. Implements request parsing, chunked transfer encoding, keep-alive, a middleware/router system, and response serialisation. Supports any stream transport that can implement these interfaces.

Packages

Package Description
packages/tsconnect The SDK: builds the WASM, exports typed TS wrappers
packages/http HTTP/1.1 server library over any stream transport
packages/test-app Vite dev app for manual browser testing

Submodules

The tailscale/ directory is a git submodule pointing to a fork of the Tailscale repository. The webnet branch on that fork contains the Go-side patches to tsconnect.

git submodule update --init tailscale

Development

# Build the WASM and TypeScript declarations
npm run build --workspace=packages/tsconnect

# Start the test app
npm run dev --workspace=packages/test-app

# Lint and format
npm run lint
npm run format

Commits must follow the Conventional Commits spec. ESLint and Prettier are also required to pass. This is enforced at commit time by husky with commitlint and lint-staged.

AI disclosure

This project was set up with the assistance of Claude Code (Anthropic). The following were written by Claude Code:

  • The tailscale submodule fork and its Go-side tsconnect patches (the webnet branch)
  • The packages/tsconnect TypeScript SDK
  • The packages/test-app Vite test application
  • The repo tooling setup (ESLint, Prettier, lint-staged, commitlint, dpdm, TypeScript 6)

The following were hand-written:

  • The packages/http HTTP/1.1 server library (HTTP parsing, server/client connection management, chunked transfer encoding, router/middleware system, response serialisation)

The following were hand-written but with substantial Claude Code contributions:

  • packages/http — low-level transport layer: reviewed and significantly reworked by Claude Code, fixing multiple bugs (errcallback not cleared after a successful read, ServerConnection draining the body after closing the connection, PooledDialer throwing instead of queuing waiters, shouldClose() doing a case-sensitive header comparison) and adding transport primitives (halfClose, readEnded, whenClosed, remoteAddr, localAddr)
  • packages/http — timeout support: the headersTimeout, keepAliveTimeout, and bodyTimeout options across both client and server were implemented by Claude Code
  • packages/http — parse error messages: improved by Claude Code to include the offending values
  • packages/http — package and build scaffolding: initial package.json, tsconfig.json, and build configuration were set up by Claude Code

The test suite for packages/http was mostly generated by Claude Code, which also identified and fixed several further bugs: a body-size-limit error being swallowed on the final body chunk, a stale idle-connection entry left behind by rejectConnection, and a read() call hanging indefinitely after a socket close.

  • packages/httphijack() on server and client responses: the hijack() method on ServerResponse and ClientResponse, the ReadBuffer.drain() helper, and the prependTransport() utility were implemented by Claude Code
  • packages/http — 1xx informational response support: implemented by Claude Code. Server side: automatic 100 Continue (sent lazily when the handler reads the body) and res.sendInformational() for 103 Early Hints etc. Client side: default skip mode, interim: "collect" to capture 1xx into res.informational[], conn.requestStream() async generator that yields each interim response and the final one as they arrive, and fetchStream() / f.stream() to expose the same streaming behaviour through the fetch API with proper connection pool management.
  • packages/http — WebSocket support: implemented by Claude Code. upgradeWebSocket(req, res) for server-side handshake; connectWebSocket(dialer, url, options?) to open a new WebSocket connection, or connectWebSocket(res, key) to promote an existing fetch()/fetchStream() 101 response — both return a WebSocketConnection async iterable. Frame codec (read/write), masking, fragmented-message reassembly, ping/pong, and the close handshake are all implemented from scratch using the Web Crypto API (crypto.subtle.digest for SHA-1, crypto.getRandomValues for mask keys) with no external dependencies. Two fixed bugs in fetch.ts were required for pool safety: a case-insensitive Connection: upgrade check and immediate pool ejection on 101 to prevent a microtask race before hijack. Exported as three tree-shakeable entry points: @webnet/http/websocket (combined), @webnet/http/websocket/client, and @webnet/http/websocket/server.
  • packages/http — 3xx redirect support: implemented by Claude Code. redirect.mode ("manual" / "same-connection" / "same-origin" / "follow"), redirect.max, redirect.filter (string array / Set / callback), redirect.credentials ("keep" / "strip-cross-origin" / "strip"), redirect.body ("resubmit" / "strip-non-resubmit" / "strip"), and redirect.collect to gather followed 3xx into res.redirects[]. In streaming mode all redirects and interims are yielded as they arrive; a drain() method was added to ClientConnection to support clean connection hand-off between redirect hops.
S
Description
No description provided
Readme 1.6 MiB
Languages
TypeScript 98%
Shell 0.6%
HTML 0.5%
JavaScript 0.5%
SCSS 0.4%