Reach another DuckDB over your Tailscale tailnet — no open ports, no TLS to manage. The extension embeds a userspace Tailscale node (tsnet) and routes DuckDB's HTTP through it, so reading from a peer just works:
-- Coming soon: signed community extensions repo, for now need to load from github
SET allow_unsigned_extensions==true;
INSTALL HTTP; LOAD HTTPFS;
INSTALL tailscale FROM 'https://smithclay.github.io/duckdb-tailscale';
LOAD tailscale;
SELECT * FROM read_csv('http://100.x.y.z:8080/data.csv'); -- over WireGuardIt's built for Quack, the remote-attach extension: with a tailnet host, ATTACH 'quack:100.x.y.z:9494' reaches a peer DuckDB with no public endpoint. Any HTTP-based remote rides the same path.
This is v0: built against DuckDB v1.5.3, proven end to end on macOS and Linux. See Limits.
The extension links a Go c-archive (libtailscale). Put a Go toolchain on PATH — or don't, and CMake fetches the pinned version into the build tree. Then:
# macOS: point at Homebrew's OpenSSL
OPENSSL_ROOT_DIR=$(brew --prefix openssl@3) GEN=ninja make releaseYou get ./build/release/duckdb with the extension built in, and ./build/release/extension/tailscale/tailscale.duckdb_extension — the loadable binary. Load it into a stock DuckDB:
duckdb -unsigned -c "LOAD '/path/to/tailscale.duckdb_extension';"Bring up an ephemeral node with a Tailscale auth key:
CALL tailscale_up(authkey := getenv('TS_AUTHKEY'), hostname := 'clienta', ephemeral := true);
SELECT * FROM tailscale_status(); -- the node's 100.x tailnet IPTo read from a peer, bridge a tailnet port to a local service on the server, then read its address on the client. Load httpfs before tailscale_up — otherwise it reclaims the HTTP slot.
-- server: forward tailnet :8080 to a local HTTP server on :8099
CALL tailscale_serve(8080, 'localhost:8099');-- client
LOAD httpfs;
CALL tailscale_up(authkey := getenv('TS_AUTHKEY'), hostname := 'clienta', ephemeral := true);
SELECT * FROM read_csv('http://100.x.y.z:8080/data.csv');Non-tailnet URLs fall through to httpfs untouched.
scripts/demo_m4.sh stands up two nodes and reads a CSV across them — static build, or stock DuckDB via LOAD:
TS_AUTHKEY=tskey-… ./scripts/demo_m4.sh
TS_LOADABLE=1 DUCKDB_BIN=duckdb ./scripts/demo_m4.sh| Function | Description |
|---|---|
tailscale_up(authkey, hostname, state_dir, control_url, ephemeral) |
Bring up the node and install the HTTP router. Named arguments. |
tailscale_status() |
One row per tailnet IP the node holds. |
tailscale_serve(port, 'host:port') |
Bridge a tailnet port to a local TCP target. |
DuckDB's HTTP stack is pluggable. tailscale_up wraps the global HTTPUtil: requests to tailnet hosts (100.64.0.0/10, *.ts.net) dial through the embedded tsnet node as plaintext HTTP/1.1 — the tailnet is the encryption layer — and everything else passes to httpfs. tailscale_serve runs the other half: a tailnet listener that bridges each connection to a local socket.
What v0 does not do yet:
- Two nodes.
tsnetwon't dial itself, so a node can't reach its own listener. httpfsfirst. Load it beforetailscale_up, or it reclaims the HTTP slot.- One request per connection.
Connection: close,Content-Length-framed responses — no keep-alive, no chunked decode. (All of DuckDB's HTTP verbs work: GET, HEAD, PUT, POST, DELETE.) No wasm —tsnethas none. - Host detection keys on the CGNAT range and
*.ts.net.
Built from the DuckDB extension template. Run make test.