derphole 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026 Shayne
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,325 @@
1
+ # derpcat
2
+
3
+ This repository ships two standalone CLIs:
4
+
5
+ - `derpcat` for raw byte streams and temporary local TCP service sharing
6
+ - `derphole` for wormhole-shaped text, file, directory, and SSH invite flows on the same transport stack
7
+
8
+ Both use the public Tailscale [DERP](#what-is-derp) relay network for rendezvous and relay fallback, but they are **not** affiliated with Tailscale, do **not** require a Tailscale account or tailnet, and do **not** use `tailscaled` for transport.
9
+
10
+ `derpcat` and `derphole` are **not** WireGuard overlays and **not** VPNs. Tailscale builds a general-purpose secure network on WireGuard. These tools are optimized for a different job: one session, one token, one transfer or shared service, on the shortest secure path they can find for that session. See [Transport Model](#transport-model), [Why It Is Fast](#why-it-is-fast), and [Security Model](#security-model).
11
+
12
+ For one-shot transfers and temporary service sharing, `derpcat` can beat sending the same traffic through a WireGuard-based overlay because it does not first build a general-purpose encrypted network path and then send application traffic through it. `derphole` uses the same session and transport machinery, but wraps it in a more human-oriented CLI. Both use DERP for rendezvous and fallback, then move the live session onto the best direct path they can establish for that workload. Details are in [Transport Model](#transport-model) and [How This Differs From Tailscale / WireGuard](#how-this-differs-from-tailscale--wireguard).
13
+
14
+ It does **not** require:
15
+
16
+ - a Tailscale account
17
+ - a tailnet
18
+ - `tailscaled`
19
+ - any separate control plane you have to run yourself
20
+
21
+ The token printed by `listen` or `share` carries session authorization. Public sessions still fetch the DERP map at runtime so both sides can find relay/bootstrap nodes. See [Security Model](#security-model) for what the token authorizes and what intermediaries can and cannot see.
22
+
23
+ ## Pick the CLI
24
+
25
+ Use `derpcat` when you want transport primitives:
26
+
27
+ - one-shot byte-stream transfer with `listen` and `send`
28
+ - long-lived local service sharing with `share` and `open`
29
+
30
+ Use `derphole` when you want wormhole-shaped workflows:
31
+
32
+ - text transfer
33
+ - file transfer
34
+ - directory transfer
35
+ - SSH public key exchange for `authorized_keys`
36
+
37
+ ## Quick Start
38
+
39
+ `listen` receives bytes and prints a token. `send` pipes bytes into that token. `share` and `open` do the same thing for a local TCP service instead of a byte stream.
40
+
41
+ ### Transfer a File
42
+
43
+ On the receiving machine:
44
+
45
+ ```bash
46
+ npx -y derpcat@latest listen > received.img
47
+ ```
48
+
49
+ `listen` prints a token to stderr. Copy that token to the sending machine.
50
+
51
+ On the sending machine:
52
+
53
+ ```bash
54
+ cat ./disk.img | npx -y derpcat@latest send <token>
55
+ ```
56
+
57
+ For a quick text example:
58
+
59
+ ```bash
60
+ printf 'hello\n' | npx -y derpcat@latest send <token>
61
+ ```
62
+
63
+ ### Send a File with `derphole`
64
+
65
+ On the sending machine:
66
+
67
+ ```bash
68
+ npx -y derphole@latest send ./photo.jpg
69
+ ```
70
+
71
+ `send` prints a command for the receiving machine. Run that command there:
72
+
73
+ ```bash
74
+ npx -y derphole@latest receive <code>
75
+ ```
76
+
77
+ Text uses the same shape:
78
+
79
+ ```bash
80
+ npx -y derphole@latest send hello
81
+ ```
82
+
83
+ Directories stream as tar on the wire and re-materialize on the receiver:
84
+
85
+ ```bash
86
+ npx -y derphole@latest send ./project-dir
87
+ ```
88
+
89
+ For SSH access exchange, the host receiving access runs:
90
+
91
+ ```bash
92
+ npx -y derphole@latest ssh invite --user deploy
93
+ ```
94
+
95
+ The other side accepts with:
96
+
97
+ ```bash
98
+ npx -y derphole@latest ssh accept <token>
99
+ ```
100
+
101
+ ### Watch Progress with `pv`
102
+
103
+ `derpcat` is plain stdin/stdout, so `pv` fits naturally in the pipe.
104
+
105
+ Install `pv` if needed:
106
+
107
+ ```bash
108
+ brew install pv
109
+ sudo apt install -y pv
110
+ ```
111
+
112
+ On the receiving machine:
113
+
114
+ ```bash
115
+ npx -y derpcat@latest listen | pv -brt > received.img
116
+ ```
117
+
118
+ On the sending machine:
119
+
120
+ ```bash
121
+ cat ./disk.img | pv -brt | npx -y derpcat@latest send <token>
122
+ ```
123
+
124
+ Want a concrete Internet/NAT version of the same idea? See [Real-World Example: Tar Pipe Over Internet](#real-world-example-tar-pipe-over-internet).
125
+
126
+ ### Share a Local TCP Service
127
+
128
+ On the machine running the local web app or API:
129
+
130
+ ```bash
131
+ npx -y derpcat@latest share 127.0.0.1:3000
132
+ ```
133
+
134
+ `share` prints a token to stderr. Copy that token to the machine that will open the shared service.
135
+
136
+ On another machine, expose that shared service locally:
137
+
138
+ ```bash
139
+ npx -y derpcat@latest open <token>
140
+ ```
141
+
142
+ `open` prints the local listening address to stderr.
143
+
144
+ Bind `open` to a specific local port if you want:
145
+
146
+ ```bash
147
+ npx -y derpcat@latest open <token> 127.0.0.1:8080
148
+ ```
149
+
150
+ ### Useful Extras
151
+
152
+ Use the development channel for the latest commit published from `main`:
153
+
154
+ ```bash
155
+ npx -y derpcat@dev version
156
+ npx -y derphole@dev version
157
+ ```
158
+
159
+ By default, `listen`, `send`, `share`, and `open` keep transport status quiet. `listen` and `share` still print the token you need, and `open` still prints the local listening address. `derphole` keeps the same quiet default and only prints the user-facing instruction or token needed to complete the transfer. Use `--verbose` to see state transitions like `connected-relay` and `connected-direct`:
160
+
161
+ ```bash
162
+ npx -y derpcat@latest --verbose listen
163
+ npx -y derphole@latest --verbose send ./photo.jpg
164
+ ```
165
+
166
+ Want transport details after the examples? Jump to [Transport Model](#transport-model), [Behavior](#behavior), or [Security Model](#security-model).
167
+
168
+ ## Transport Model
169
+
170
+ High level:
171
+
172
+ 1. `listen` or `share` creates an ephemeral session and prints an opaque bearer token.
173
+ 2. That token contains the session ID, expiry, DERP bootstrap hints, the listener's public peer identity, a bearer secret, and the allowed session capability.
174
+ 3. `send` or `open` uses the token to contact the listener through DERP and claim the session.
175
+ 4. The listener validates the claim, checks the requested capability, and returns its current direct-path candidates.
176
+ 5. Both sides start on the first working path immediately, including DERP relay if direct connectivity is not ready yet.
177
+ 6. In parallel, both sides continue NAT traversal and direct-path probing. If a direct path succeeds, the live session upgrades in place without restarting the transfer.
178
+
179
+ ### Data Plane Selection
180
+
181
+ DERP is used for **rendezvous** and **relay fallback**. If the term is new, see [What Is DERP?](#what-is-derp):
182
+
183
+ - rendezvous: exchange initial claim, decision, and direct-path coordination messages without a separate account-backed control plane
184
+ - relay fallback: keep the session working even when NAT traversal fails or a direct path is not ready yet
185
+
186
+ The data plane is selected per session:
187
+
188
+ - `share/open` uses multiplexed QUIC streams over `derpcat`'s relay/direct UDP transport, so one claimed session can carry many independent TCP connections to the shared service.
189
+ - `listen/send` uses a one-shot byte stream. By default, `derpcat` coordinates through DERP, promotes to a rate-adaptive direct UDP blast when traversal succeeds, and stays on encrypted relay fallback when no direct path is available.
190
+
191
+ Candidate discovery splits into two phases:
192
+
193
+ - fast local candidates first: immediately advertise local socket/interface candidates and any cached port mapping
194
+ - background traversal discovery: run STUN and UPnP / NAT-PMP / PCP refresh, then send updated candidates and `call-me-maybe` probes if a new direct endpoint appears
195
+
196
+ That keeps startup latency low while still allowing relay-to-direct promotion.
197
+
198
+ ## How This Differs From Tailscale / WireGuard
199
+
200
+ Tailscale uses WireGuard to build a secure general-purpose network between peers. That is the right abstraction when you want durable machine-to-machine connectivity, stable private addressing, ACLs, subnet routing, exit nodes, and a long-lived encrypted overlay.
201
+
202
+ `derpcat` does something narrower and faster for its target workload. It creates session-scoped transport for a single transfer or a single shared service:
203
+
204
+ - no WireGuard tunnel device
205
+ - no overlay network interface
206
+ - no persistent mesh control plane
207
+ - no need to route arbitrary traffic through a general encrypted network
208
+
209
+ Instead, `derpcat` uses a bearer token to authorize exactly one session, uses DERP to get both peers talking immediately, and then promotes the session onto the best direct path it can establish for that workload. Supporting details are in [Transport Model](#transport-model) and [Security Model](#security-model).
210
+
211
+ For `send/listen` and `share/open`, that can beat routing the same traffic through a WireGuard-based overlay because `derpcat` is purpose-built for the active session, not for a general secure network abstraction. See [Why It Is Fast](#why-it-is-fast) for the concrete transport reasons.
212
+
213
+ ## Why It Is Fast
214
+
215
+ `derpcat` gets its performance from the transport design:
216
+
217
+ - DERP is for rendezvous and relay fallback, not the preferred steady-state data plane.
218
+ - Sessions can start relayed immediately, then promote in place to direct without restarting the transfer.
219
+ - `listen/send` can scale from one to multiple direct UDP lanes, runs a short path-rate probe, then uses paced sending, adaptive rate control, and targeted replay/repair. That lets fast links run near their WAN ceiling without forcing slower links into the same send rate.
220
+ - Direct UDP payload packets are AEAD-protected with a per-session key derived from the bearer secret. The packet header stays visible for sequencing and repair, while user bytes stay encrypted and authenticated.
221
+ - `share/open` keeps QUIC stream multiplexing for service sharing, where many independent TCP streams need one claimed session.
222
+ - Candidate discovery is front-loaded with local interface candidates and cached mappings, then refined in the background with STUN and port mapping refresh. That keeps the first byte moving quickly instead of stalling the session until every traversal probe finishes.
223
+
224
+ In practice: get bytes moving early, keep them moving through relay if needed, then shift the live session onto a faster direct path as soon as direct connectivity is ready.
225
+
226
+ ## Security Model
227
+
228
+ The session token is a **bearer capability**. Anyone who has the token can claim the session until it expires, so share it over a channel you trust. Tokens expire after one hour.
229
+
230
+ DERP relays do **not** get the secret material needed to read or impersonate the session:
231
+
232
+ - On the default `listen/send` direct UDP path, payload packets are encrypted and authenticated with session AEAD derived from the bearer secret in the token.
233
+ - On `share/open`, stream traffic is carried over authenticated QUIC streams for the claimed session.
234
+ - If packets are relayed through DERP, DERP only forwards encrypted session bytes.
235
+
236
+ Important security property: `derpcat` does not trade speed for plaintext shortcuts:
237
+
238
+ - the token authorizes the session, but does not turn DERP into a trusted decrypting proxy
239
+ - direct UDP data packets are encrypted and authenticated per session
240
+ - QUIC stream-mode peers are pinned to the expected public identity from the token
241
+ - DERP forwards encrypted traffic but does not have the keys required to decrypt or impersonate the session
242
+
243
+ Simple rule: possession of the token authorizes the session, but intermediaries that only see DERP traffic do not have the keys needed to decrypt it.
244
+
245
+ ## Behavior
246
+
247
+ Sessions can start on DERP relay and later promote to a direct path without restarting. In default mode, the CLI keeps transport status quiet and only prints the token or local bind address needed to use the session. Use `--verbose` to inspect path changes, NAT traversal state, and direct-path tuning details.
248
+
249
+ ## Use Cases
250
+
251
+ - easy cross-host transfer with no account setup
252
+ - useful behind NATs where direct connectivity may or may not work
253
+ - good for quick sharing of local web apps, APIs, and admin interfaces
254
+ - can be used entirely through `npx` without a manual install
255
+
256
+ ## Real-World Example: Tar Pipe Over Internet
257
+
258
+ Classic tar pipe is fast because it streams bytes directly from `tar` on one host into `tar` on another host. Good reference: [Using netcat and tar to quickly transfer files between machines, aka tar pipe](https://toast.djw.org.uk/tarpipe.html).
259
+
260
+ Problem: classic `tar | nc` assumes receiver can expose a listening port and sender can reach it. That breaks down fast when both hosts are on the public Internet, both sit behind NAT, and neither side should expose an inbound port.
261
+
262
+ `derpcat` keeps the same streaming shape, but removes the open-port requirement.
263
+
264
+ Receiver:
265
+
266
+ ```bash
267
+ npx -y derpcat@latest listen | tar -xpf - -C /restore/path
268
+ ```
269
+
270
+ `listen` prints a token on stderr. Copy that token to the sender over a channel you trust.
271
+
272
+ Sender:
273
+
274
+ ```bash
275
+ tar -cpf - /srv/data | npx -y derpcat@latest send <token>
276
+ ```
277
+
278
+ This is still tar pipe. Difference: no public listener to expose, no SSH daemon required for data path, no VPN to join, and no permanent mesh to set up. `derpcat` starts with DERP if needed, then promotes the live transfer onto direct UDP when a faster direct path becomes available.
279
+
280
+ ## Development
281
+
282
+ ```bash
283
+ mise install
284
+ mise run install-githooks
285
+ mise run check
286
+ mise run build
287
+ ```
288
+
289
+ `mise run build` writes both `dist/derpcat` and `dist/derphole`.
290
+
291
+ ## Verification
292
+
293
+ Local smoke test:
294
+
295
+ ```bash
296
+ mise run smoke-local
297
+ ```
298
+
299
+ Remote smoke tests against a host you control:
300
+
301
+ ```bash
302
+ REMOTE_HOST=my-server.example.com mise run smoke-remote
303
+ REMOTE_HOST=my-server.example.com mise run smoke-remote-share
304
+ REMOTE_HOST=my-server.example.com mise run promotion-1g
305
+ ```
306
+
307
+ ## Releases
308
+
309
+ - npm packages: `derpcat`, `derphole`
310
+ - production channel: `@latest` on each package
311
+ - development channel: `@dev` on each package
312
+ - bootstrap runbook: [docs/releases/npm-bootstrap.md](docs/releases/npm-bootstrap.md)
313
+
314
+ ## What Is DERP?
315
+
316
+ DERP stands for **Designated Encrypted Relay for Packets**. In plain terms, it is a globally reachable relay network that both peers can talk to even when they cannot yet talk directly to each other.
317
+
318
+ DERP was built by Tailscale for the Tailscale networking stack, and the public Tailscale-operated DERP network is reachable without running your own relays. The same DERP model is also used by Headscale, the open-source Tailscale control server implementation, which can serve its own DERP map and DERP servers.
319
+
320
+ In `derpcat`, DERP has two jobs:
321
+
322
+ - rendezvous: carry the initial claim, decision, and direct-path coordination messages so the two peers can find each other without a separate account-backed control plane
323
+ - fallback relay: carry encrypted session traffic when NAT traversal has not succeeded yet or when direct connectivity is unavailable
324
+
325
+ DERP is not the preferred steady-state path. It is the safety net that gets the session started and keeps it working. If a direct UDP path becomes available, `derpcat` promotes the live session onto that direct path. DERP only forwards bytes; it does not get the session keys needed to decrypt the traffic.
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { existsSync } from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const triples = new Map([
13
+ ["linux:x64", "x86_64-unknown-linux-musl"],
14
+ ["linux:arm64", "aarch64-unknown-linux-musl"],
15
+ ["darwin:x64", "x86_64-apple-darwin"],
16
+ ["darwin:arm64", "aarch64-apple-darwin"]
17
+ ]);
18
+
19
+ const triple = triples.get(`${process.platform}:${process.arch}`);
20
+ if (!triple) {
21
+ console.error(`Unsupported platform: ${process.platform} (${process.arch})`);
22
+ process.exit(1);
23
+ }
24
+
25
+ const binaryName = process.platform === "win32" ? "derphole.exe" : "derphole";
26
+ const binaryPath = path.join(__dirname, "..", "vendor", triple, "derphole", binaryName);
27
+ if (!existsSync(binaryPath)) {
28
+ console.error(`Missing vendored binary: ${binaryPath}`);
29
+ process.exit(1);
30
+ }
31
+
32
+ const child = spawn(binaryPath, process.argv.slice(2), {
33
+ stdio: "inherit",
34
+ env: { ...process.env, DERPHOLE_MANAGED_BY_NPM: "1" }
35
+ });
36
+
37
+ child.on("error", (err) => {
38
+ const reason = err instanceof Error ? err.message : String(err);
39
+ console.error(`Failed to launch vendored binary: ${reason}`);
40
+ process.exit(1);
41
+ });
42
+
43
+ ["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => {
44
+ process.on(sig, () => {
45
+ if (!child.killed) {
46
+ child.kill(sig);
47
+ }
48
+ });
49
+ });
50
+
51
+ const result = await new Promise((resolve) => {
52
+ child.on("exit", (code, signal) => {
53
+ if (signal) {
54
+ resolve({ signal });
55
+ return;
56
+ }
57
+ resolve({ code: code ?? 1 });
58
+ });
59
+ });
60
+
61
+ if (result.signal) {
62
+ const signalNumber = os.constants.signals[result.signal];
63
+ process.exit(typeof signalNumber === "number" ? 128 + signalNumber : 1);
64
+ } else {
65
+ process.exit(result.code);
66
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "derphole",
3
+ "version": "0.0.1",
4
+ "license": "BSD-3-Clause",
5
+ "bin": {
6
+ "derphole": "bin/derphole.js"
7
+ },
8
+ "type": "module",
9
+ "os": [
10
+ "linux",
11
+ "darwin"
12
+ ],
13
+ "cpu": [
14
+ "x64",
15
+ "arm64"
16
+ ],
17
+ "engines": {
18
+ "node": ">=16"
19
+ },
20
+ "files": [
21
+ "bin",
22
+ "vendor",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/shayne/derpcat.git"
29
+ }
30
+ }