Bugs: - ReadableHttpImpl.bytes() was copying data byte-by-byte; use set() - NodeTransport and LoopbackTransportHalf were not clearing #errcallback after a successful read(), causing errors between reads to be silently swallowed; also gate #maybeEnded to only fire when a read() is pending - ServerConnection.handle() was draining the request body after closing the connection; skip the drain entirely on close (it only matters for keep-alive reuse), and close after draining otherwise - PooledDialer threw "Pool full" / "Per-origin full" instead of queuing waiters; implement queuing via slot.waiting and a new #globalWaiting list; also fix #shutdown never being set to true in shutdown(), and reserve the slot before the async connect to prevent races Code quality: - shouldClose() was comparing Connection header values case-sensitively; RFC 7230 requires case-insensitive comparison - Headers._normalized was public (convention only); make it protected - Headers.headers returned the original wire-case record while MutableHeaders.headers returned lowercase keys; unify both to return from _normalized so ServerRequest.headers and ServerResponse.headers are consistent (both lowercase) Enhancements: - RawTransport: add optional halfClose(), readEnded, whenClosed, remoteAddr, localAddr; NodeTransport and LoopbackTransportHalf implement all applicable members; addresses cached at construction before the socket handle can be invalidated - RawListener: add optional addr; NodeListener exposes the bound address (critical when listening on port 0) - LoopbackTransportHalf: internalize _closeFn/_writeFn as constructor parameters stored in private fields Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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:
- Patching tsconnect: the
tailscalesubmodule tracks a fork on thewebnetbranch 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. @webnet/tsconnect: an npm package (packages/tsconnect) that builds the WASM artifact, ships it alongside a Mozilla CA bundle andwasm_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.@webnet/test-app: a Vite dev app (packages/test-app) for manual browser testing. It exposesinitIPN,wasmURL,cacertURL, andloadCACertsonwindowso the full stack can be exercised from the browser console without writing any test code.@webnet/http: an HTTP/1.1 server library (packages/http) built on top of theRawTransport/RawListenerprimitives 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
tailscalesubmodule fork and its Go-sidetsconnectpatches (thewebnetbranch) - The
packages/tsconnectTypeScript SDK - The
packages/test-appVite test application - The repo tooling setup (ESLint, Prettier, lint-staged, commitlint, dpdm, TypeScript 6)
The following were hand written, but with support from Claude Code for spec guidance, sanity checking and bug spotting:
- The
packages/httpHTTP/1.1 server library