Query a DuckDB behind NAT from anywhere, over an encrypted peer-to-peer connection
Installing and Loading
INSTALL quackhole FROM community;
LOAD quackhole;
Example
-- quackhole is a transport bridge to connect duckdb instances
-- running anywhere.
-- On the machine you want to reach, behind NAT or a firewall:
INSTALL quack; LOAD quack;
LOAD quackhole;
-- accept external connections that provide a special token
FROM quackhole_serve(token := 'your-shared-token');
-- From anywhere else -- a different network, a different continent.
INSTALL quack; LOAD quack;
LOAD quackhole;
CALL quackhole_attach('qh1_...', name := 'laptop');
FROM laptop.logs WHERE ts > now() - INTERVAL '1 hour';
-- Roles are per call, not per machine: one DuckDB can serve and attach at
-- the same time. To run both on ONE machine, set a throwaway key first --
-- otherwise both load the same key, share an endpoint id, and iroh refuses
-- the dial with "connecting to ourself is not supported":
-- SET GLOBAL quackhole_ephemeral = true;
About quackhole
Use it for
- Querying a DuckDB behind NAT with no inbound port open on either side
- Joining across several remote DuckDBs at once, since roles are per call
- Restricting who may connect with an
allowlist of endpoint ids, checked before any Quack traffic - Reaching a laptop from a browser — DuckDB-Wasm can attach to an
unmodified
quackhole_serve
Interface
| Function | What it does |
|---|---|
quackhole_serve([token], [target], [allow], [ephemeral], [auto_serve]) |
Start Quack on target (default 127.0.0.1:9494) if needed, bind an iroh endpoint, accept streams into it. Returns the ticket and a workbench link |
quackhole_attach(ticket, [name]) |
Secret, scope, relay and ATTACH for the peer a ticket names, under the catalog name (default remote) |
quackhole_stop() |
Stop the accept loop. Cached outbound connections stay usable |
quackhole_status() |
Endpoint id, relay URL, whether serving, and one row per known peer with its path (direct or relay) |
Settings are read when the endpoint binds, which can happen implicitly on the
first ATTACH to a .iroh host, so set them globally: quackhole_key_path
(default ~/.quackhole/key), quackhole_ephemeral, quackhole_relay_url.
Security
The endpoint key at ~/.quackhole/key (mode 0600) is the address.
Persisting it is what lets an address survive a restart; losing it means a new
address, not a disclosure of data at rest.
iroh guarantees that the server is who the address says and that the relay can
neither read nor forge traffic. Quackhole adds the optional allow list and a
fixed target, so only the Quack port is reachable. Quack itself governs which
SQL runs. Relays see endpoint ids, timing, and byte counts — not SQL, results,
tokens, or which database is attached.
Good to know
- Early-stage. Measured over a public relay between networks with no route
between them:
ATTACHabout a second, warm queries 0.14–0.21s, a 200k-row scan under 2.5s. - Hole punching is unverified. Every measurement so far is relayed, so nothing yet says how often iroh gets a direct connection through real CGNAT or a symmetric NAT. Expect relay latency until you have measured otherwise.
- Quackhole carries Quack traffic only.
read_csv('https://<id>.iroh:9494/x.csv')reaches httpfs, not quackhole; useATTACHand SQL. - Browsers are client-only and relay-only, and need cross-origin isolation (COOP/COEP).
- An idle
ATTACHholds a relay path open at roughly one packet every five seconds, and cached connections are never evicted. - Native only: macOS, Linux, Windows. This is not a wasm extension.
Added Functions
| function_name | function_type | description | comment | examples |
|---|---|---|---|---|
| quackhole_attach | table | NULL | NULL | |
| quackhole_serve | table | NULL | NULL | |
| quackhole_status | table | NULL | NULL | |
| quackhole_stop | table | NULL | NULL |
Overloaded Functions
This extension does not add any function overloads.
Added Types
This extension does not add any types.
Added Settings
| name | description | input_type | scope | aliases |
|---|---|---|---|---|
| quackhole_ephemeral | Use a throwaway endpoint key instead of the persisted one | BOOLEAN | GLOBAL | [] |
| quackhole_key_path | Path to the persisted iroh endpoint key (default: ~/.quackhole/key) | VARCHAR | GLOBAL | [] |
| quackhole_relay_url | Relay to reach peers through, skipping address lookup (default: look up) | VARCHAR | GLOBAL | [] |
| quackhole_relay_wait_ms | How long quackhole_serve() waits for a home relay before returning a NULL ticket | BIGINT | GLOBAL | [] |
| quackhole_relays | Relay servers this endpoint homes on, comma-separated (default: n0's public relays) | VARCHAR | GLOBAL | [] |
| quackhole_workbench_url | Workbench URL that quackhole_serve()'s url column points at (default: https://smithclay.github.io/quackhole/) | VARCHAR | GLOBAL | [] |