Skip to content

smithclay/duckdb-tailscale

Repository files navigation

duckdb-tailscale

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 WireGuard

It'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.

Build

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 release

You 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';"

Use it

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 IP

To 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

Functions

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.

How it works

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.

Limits

What v0 does not do yet:

  • Two nodes. tsnet won't dial itself, so a node can't reach its own listener.
  • httpfs first. Load it before tailscale_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 — tsnet has none.
  • Host detection keys on the CGNAT range and *.ts.net.

Built from the DuckDB extension template. Run make test.

About

connect your data (or other duckdb instances via quack) to duckdb over a tailscale network

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Contributors

Generated from duckdb/extension-template