Find your next open-source project

Search by name, purpose, or keyword — or paste a GitHub repository URL

littledivy/tailnet-do

DevOps & Self-hosting

Run a Tailscale node as a Cloudflare Durable Object with its own 100.x address.

103
2
JavaScript
1 day old

What it does

tailnet-do turns a Cloudflare Durable Object into a regular device on a Tailscale tailnet. The object gets its own 100.x address and can serve HTTP or TCP to other tailnet devices, dial into private machines, and act as an exit node or subnet router.

The implementation is Tailscale's userspace client (WireGuard, netstack, DERP) compiled to WebAssembly. Control plane and DERP relays are reached over outbound WebSockets; traffic leaving through an exit node goes out via cloudflare:sockets.

Quick start

Set the auth key and other secrets on the Worker, then subclass TailnetObject and implement onTailnet to serve traffic:

import { TailnetObject } from "tailnet-do";

export class Node extends TailnetObject {
  async onTailnet(tailnet) {
    tailnet.serve(80, (req, who) => new Response(`hi ${who.loginName}`));
  }
}

export default {
  async fetch(req, env) {
    const node = env.NODE.getByName("main");
    return node.tailnetFetch("http://homelab:8080/status");
  },
};

Configure TS_AUTHKEY (and optionally TS_HOSTNAME, TS_CONTROL_URL) as secrets. Without an auth key the node starts an interactive login and exposes the URL as status().loginURL.

To build the WebAssembly engine from source, initialize the pinned Tailscale submodule first, then build and run the exit-node example:

git submodule update --init --depth 1
npm run build
cd examples/exit-node && npx wrangler dev

API surface

Inside a Durable Object, Tailnet.start(ctx, options) returns a node; TailnetObject wraps it. connect("host:port") opens TCP to a tailnet host (MagicDNS names work), fetch(url, init) sends HTTP over the tailnet, serve(port, handler) runs an HTTP server on the node's tailnet address, and listen(port, handler) accepts raw TCP. whois, status, ready and logout cover identity, state and session management. TailnetObject also exposes tailnetConnect, tailnetFetch and tailnetStatus as RPC methods.

Options include authKey, hostname, controlURL, ephemeral, tags, exitNode, advertiseRoutes, dohURL, keepAlive, statePrefix and onLog.

Limits

An exit node only forwards TCP plus DNS answered over DoH; Workers have no UDP sockets, so QUIC falls back to TCP. Outbound port 25 is blocked, and outbound sockets to Cloudflare IP ranges may be blocked. A Durable Object lives in one location, so traffic exits from where the object was created; use locationHint to place exit nodes in specific regions.

The repository declares no license and ships no release or package metadata in the provided material, so relying on the published npm import or deploying it long-term requires checking those details yourself.

HotRepo's take

The project appeared on HotRepo's new-project list on 2026-11-08, recording 103 stars and 2 forks, with a one-hour growth window of 3 stars. That single snapshot is enough to say it drew early attention but not to establish a trend. The interesting part is architectural: it puts a full userspace Tailscale client, including WireGuard and DERP, inside a Cloudflare Durable Object, which makes it a practical way to bridge Workers to private tailnet services without a separate gateway machine.

Project source and documentation

Written by AI from the project's README, updated . The project's own docs are the source of truth.