derptun 0.0.0-dev.20260418025620.6a4cabb

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,370 @@
1
+ # derphole
2
+
3
+ `derphole` is a standalone CLI for session-scoped transfers and temporary local TCP service sharing.
4
+
5
+ It supports:
6
+
7
+ - raw byte streams with `listen` and `pipe`
8
+ - text, file, and directory transfer with `send` and `receive`
9
+ - local TCP service sharing with `share` and `open`
10
+ - SSH access exchange with `ssh invite` and `ssh accept`
11
+ - durable TCP tunnels with the companion `derptun` package
12
+
13
+ `derphole` uses the public Tailscale [DERP](#what-is-derp) relay network for rendezvous and relay fallback. It is **not** affiliated with Tailscale, does **not** require a Tailscale account or tailnet, and does **not** use `tailscaled` for transport.
14
+
15
+ `derphole` is **not** a WireGuard overlay and **not** a VPN. Tailscale builds a general-purpose secure network on WireGuard. `derphole` is optimized for a different job: one session, one token, one transfer or one shared service, on the shortest secure path it can find for that session. See [Transport Model](#transport-model), [Why It Is Fast](#why-it-is-fast), and [Security Model](#security-model).
16
+
17
+ For one-shot transfers and temporary service sharing, `derphole` can beat sending the same traffic through a WireGuard-based overlay. It does not build a general-purpose encrypted network path first, then route application traffic through it. It uses DERP for rendezvous and fallback, then moves the live session onto the best direct path it 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).
18
+
19
+ It does **not** require:
20
+
21
+ - a Tailscale account
22
+ - a tailnet
23
+ - `tailscaled`
24
+ - a separate control plane to run yourself
25
+
26
+ Session tokens carry authorization, and receive-code flows resolve into the same session model. Public sessions fetch the DERP map at runtime so both sides can find relay and bootstrap nodes. See [Security Model](#security-model) for what the token authorizes and what intermediaries can and cannot see.
27
+
28
+ ## Pick the Workflow
29
+
30
+ - Use `listen` and `pipe` for raw byte streams and shell pipelines.
31
+ - Use `send` and `receive` for text, files, directories, progress, and receive-code UX.
32
+ - Use `share` and `open` for temporary access to a local TCP service.
33
+ - Use `ssh invite` and `ssh accept` for SSH public key exchange.
34
+ - Use `derptun` for durable TCP tunnels with reusable, longer-lived tokens.
35
+
36
+ ## Quick Start
37
+
38
+ `listen` receives bytes and prints a token. `pipe` sends stdin into that token. `share` and `open` use the same token shape for local TCP services. Use `derptun` when the service needs a reusable token and longer-lived tunnel.
39
+
40
+ ### Stream a Raw File
41
+
42
+ On the receiving machine:
43
+
44
+ ```bash
45
+ npx -y derphole@latest listen > received.img
46
+ ```
47
+
48
+ `listen` prints a token to stderr so stdout stays clean for received bytes. Copy that token to the sending machine.
49
+
50
+ On the sending machine:
51
+
52
+ ```bash
53
+ cat ./disk.img | npx -y derphole@latest pipe <token>
54
+ ```
55
+
56
+ For quick text:
57
+
58
+ ```bash
59
+ printf 'hello\n' | npx -y derphole@latest pipe <token>
60
+ ```
61
+
62
+ ### Send with a Receive Code
63
+
64
+ On the sending machine:
65
+
66
+ ```bash
67
+ npx -y derphole@latest send ./photo.jpg
68
+ ```
69
+
70
+ `send` prints the command to run on the receiving machine:
71
+
72
+ ```bash
73
+ npx -y derphole@latest receive <code>
74
+ ```
75
+
76
+ For known-size file and directory transfers, `derphole` prints wormhole-shaped progress and rate output on stderr. Use `--hide-progress` for quiet output.
77
+
78
+ Text uses the same flow:
79
+
80
+ ```bash
81
+ npx -y derphole@latest send hello
82
+ ```
83
+
84
+ Directories stream as tar on the wire and re-materialize on the receiver:
85
+
86
+ ```bash
87
+ npx -y derphole@latest send ./project-dir
88
+ ```
89
+
90
+ ### Exchange SSH Access
91
+
92
+ The host receiving access runs:
93
+
94
+ ```bash
95
+ npx -y derphole@latest ssh invite --user deploy
96
+ ```
97
+
98
+ The other side accepts with:
99
+
100
+ ```bash
101
+ npx -y derphole@latest ssh accept <token>
102
+ ```
103
+
104
+ ### Watch Progress with `pv`
105
+
106
+ `derphole` is plain stdin/stdout, so `pv` fits naturally in the pipeline.
107
+
108
+ Install `pv` if needed:
109
+
110
+ ```bash
111
+ brew install pv
112
+ sudo apt install -y pv
113
+ ```
114
+
115
+ On the receiving machine:
116
+
117
+ ```bash
118
+ npx -y derphole@latest listen | pv -brt > received.img
119
+ ```
120
+
121
+ On the sending machine:
122
+
123
+ ```bash
124
+ cat ./disk.img | pv -brt | npx -y derphole@latest pipe <token>
125
+ ```
126
+
127
+ For a concrete Internet/NAT version of the same pattern, see [Real-World Example: Tar Pipe Over Internet](#real-world-example-tar-pipe-over-internet).
128
+
129
+ ### Share a Local TCP Service
130
+
131
+ On the machine running the local web app or API:
132
+
133
+ ```bash
134
+ npx -y derphole@latest share 127.0.0.1:3000
135
+ ```
136
+
137
+ `share` prints a token to stderr. Copy that token to the machine that needs access.
138
+
139
+ On another machine, expose the shared service locally:
140
+
141
+ ```bash
142
+ npx -y derphole@latest open <token>
143
+ ```
144
+
145
+ `open` prints the local listening address to stderr.
146
+
147
+ Bind `open` to a specific local port:
148
+
149
+ ```bash
150
+ npx -y derphole@latest open <token> 127.0.0.1:8080
151
+ ```
152
+
153
+ ### Durable SSH Tunnels with `derptun`
154
+
155
+ `derptun` is the durable TCP tunnel companion to `derphole`. Use it when a host is behind NAT and you want a stable token you can reuse for days instead of a one-hour, session-scoped share token.
156
+
157
+ On the target host:
158
+
159
+ ```bash
160
+ npx -y derptun@latest token --days 7 > alpha.token
161
+ npx -y derptun@latest serve --token "$(cat alpha.token)" --tcp 127.0.0.1:22
162
+ ```
163
+
164
+ On the client:
165
+
166
+ ```bash
167
+ npx -y derptun@latest open --token "$(cat alpha.token)" --listen 127.0.0.1:2222
168
+ ssh -p 2222 foo@127.0.0.1
169
+ ```
170
+
171
+ For SSH without a separate local listener, use `ProxyCommand`:
172
+
173
+ ```sshconfig
174
+ Host alpha-derptun
175
+ HostName alpha
176
+ User foo
177
+ ProxyCommand derptun connect --token ~/.config/derptun/alpha.token --stdio
178
+ ```
179
+
180
+ `derptun` keeps trying when the network path drops, and it can reconnect while both `derptun` processes stay alive. If either process exits, the token can bring the tunnel back, but an already-open TCP session is gone. Use `tmux` or `screen` on the remote host when shell continuity matters.
181
+
182
+ Tokens default to seven days. Set a relative lifetime with `--days`, or use an absolute expiry:
183
+
184
+ ```bash
185
+ npx -y derptun@latest token --expires 2026-05-01T00:00:00Z
186
+ ```
187
+
188
+ The first `derptun` release is TCP-only. UDP forwarding is planned for use cases like Minecraft Bedrock servers, but it is not part of this release.
189
+
190
+ ### Useful Extras
191
+
192
+ Use the development channel for the latest commit from `main`:
193
+
194
+ ```bash
195
+ npx -y derphole@dev version
196
+ npx -y derptun@dev version
197
+ ```
198
+
199
+ By default, `listen`, `pipe`, `send`, `receive`, `share`, and `open` keep transport status quiet. `listen` and `share` print tokens, `open` prints the local listening address, and `send` / `receive` print the receiver command or code needed to complete the transfer. Known-size transfers show wormhole-shaped progress on stderr. Use `--hide-progress` to suppress the progress bar. Use `--verbose` to see state transitions like `connected-relay` and `connected-direct`:
200
+
201
+ ```bash
202
+ npx -y derphole@latest --verbose listen
203
+ npx -y derphole@latest --verbose pipe <token>
204
+ npx -y derphole@latest --verbose send ./photo.jpg
205
+ ```
206
+
207
+ For transport details, see [Transport Model](#transport-model), [Behavior](#behavior), or [Security Model](#security-model).
208
+
209
+ ## Transport Model
210
+
211
+ High-level flow:
212
+
213
+ 1. `listen`, `share`, or `receive` creates an ephemeral session and prints an opaque bearer token or receive code.
214
+ 2. The token carries the session ID, expiry, DERP bootstrap hints, the listener's public peer identity, bearer secret, and allowed capability.
215
+ 3. `pipe`, `send`, or `open` uses that token to contact the listener through DERP and claim the session.
216
+ 4. The listener validates the claim, checks the requested capability, and returns current direct-path candidates.
217
+ 5. Both sides start on the first working path, including DERP relay if direct connectivity is not ready yet.
218
+ 6. Both sides keep probing for a better direct path. If a direct path succeeds, the live session upgrades in place without restarting the transfer.
219
+
220
+ ### Data Plane Selection
221
+
222
+ DERP provides **rendezvous** and **relay fallback**. If the term is new, see [What Is DERP?](#what-is-derp):
223
+
224
+ - rendezvous: exchange initial claim, decision, and direct-path coordination messages without an account-backed control plane
225
+ - relay fallback: keep the session working when NAT traversal fails or direct connectivity is not ready yet
226
+
227
+ The data plane is selected per session:
228
+
229
+ - `share/open` uses multiplexed QUIC streams over `derphole`'s relay/direct UDP transport, so one claimed session can carry many independent TCP connections to the shared service.
230
+ - `derptun` uses a stable tunnel token and the same relay/direct UDP transport to carry reconnectable TCP streams. It is built for longer-lived access, such as SSH to a host behind NAT.
231
+ - `listen/pipe` uses a one-shot byte stream. By default, `derphole` coordinates through DERP, promotes to rate-adaptive direct UDP when traversal succeeds, and stays on encrypted relay fallback when no direct path is available.
232
+ - `send/receive` wraps the same one-shot stream with text, file, directory, and progress metadata.
233
+
234
+ Candidate discovery splits into two phases:
235
+
236
+ - fast local candidates first: advertise local socket/interface candidates and any cached port mapping immediately
237
+ - background traversal discovery: run STUN and UPnP / NAT-PMP / PCP refresh, then send updated candidates and `call-me-maybe` probes when a new direct endpoint appears
238
+
239
+ This keeps startup latency low while still allowing relay-to-direct promotion.
240
+
241
+ ## How This Differs From Tailscale / WireGuard
242
+
243
+ Tailscale uses WireGuard to build a secure general-purpose network between peers. That is the right abstraction for durable machine-to-machine connectivity, stable private addressing, ACLs, subnet routing, exit nodes, and a long-lived encrypted overlay.
244
+
245
+ `derphole` does something narrower. It creates session-scoped transport for a single transfer or one shared service:
246
+
247
+ - no WireGuard tunnel device
248
+ - no overlay network interface
249
+ - no persistent mesh control plane
250
+ - no need to route arbitrary traffic through a general encrypted network
251
+
252
+ Instead, `derphole` uses a bearer token to authorize one session, uses DERP to get both peers talking immediately, and 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).
253
+
254
+ For `listen/pipe`, `send/receive`, and `share/open`, this can beat routing the same traffic through a WireGuard-based overlay because `derphole` is purpose-built for the active session, not for a general secure network abstraction. See [Why It Is Fast](#why-it-is-fast) for concrete transport reasons.
255
+
256
+ ## Why It Is Fast
257
+
258
+ `derphole` gets performance from its transport design:
259
+
260
+ - DERP is for rendezvous and relay fallback, not the preferred steady-state data plane.
261
+ - Sessions can start relayed immediately, then promote in place to direct without restarting the transfer.
262
+ - `listen/pipe` and `send/receive` can scale from one to multiple direct UDP lanes, run a short path-rate probe, then use paced sending, adaptive rate control, and targeted replay/repair. Fast links can run near their WAN ceiling without forcing slower links into the same send rate.
263
+ - 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.
264
+ - `share/open` keeps QUIC stream multiplexing for service sharing, where many independent TCP streams need one claimed session.
265
+ - 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.
266
+
267
+ In practice: move bytes 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.
268
+
269
+ ## Security Model
270
+
271
+ Tokens are **bearer capabilities**. Anyone with a token can claim the matching session or tunnel until it expires, so share tokens over a trusted channel. `derphole` session tokens expire after one hour. `derptun` tokens default to seven days and can be shortened or extended with `--days` or `--expires`.
272
+
273
+ DERP relays do **not** get the secret material needed to read or impersonate the session:
274
+
275
+ - On the default `listen/pipe` and `send/receive` direct UDP path, payload packets are encrypted and authenticated with session AEAD derived from the token bearer secret.
276
+ - On `share/open`, stream traffic uses authenticated QUIC streams for the claimed session.
277
+ - On `derptun`, stream traffic uses authenticated QUIC streams pinned to the stable tunnel identity in the token.
278
+ - If packets are relayed through DERP, DERP only forwards encrypted session bytes.
279
+
280
+ Important security property: `derphole` does not trade speed for plaintext shortcuts:
281
+
282
+ - the token authorizes the session, but does not turn DERP into a trusted decrypting proxy
283
+ - direct UDP data packets are encrypted and authenticated per session
284
+ - QUIC stream-mode peers are pinned to the expected public identity from the token
285
+ - DERP forwards encrypted traffic but does not have the keys required to decrypt or impersonate the session
286
+
287
+ Simple rule: token possession authorizes the session, but intermediaries that only see DERP traffic do not have the keys needed to decrypt it.
288
+
289
+ ## Behavior
290
+
291
+ Sessions can start on DERP relay and later promote to a direct path without restarting. By default, the CLI keeps transport status quiet and prints only the user-facing token, bind address, or transfer UI needed to use the session. Use `--verbose` to inspect path changes, NAT traversal state, and direct-path tuning.
292
+
293
+ ## Use Cases
294
+
295
+ - cross-host transfer with no account setup
296
+ - NAT-heavy networks where direct connectivity may or may not work
297
+ - quick sharing of local web apps, APIs, and admin interfaces
298
+ - `npx` use without manual install
299
+
300
+ ## Real-World Example: Tar Pipe Over Internet
301
+
302
+ Classic tar pipe is fast because it streams bytes 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).
303
+
304
+ Problem: classic `tar | nc` assumes the receiver can expose a listening port and the sender can reach it. That breaks down when both hosts are on the public Internet, both sit behind NAT, and neither side should expose an inbound port.
305
+
306
+ `derphole` keeps the same streaming shape, but removes the open-port requirement.
307
+
308
+ Receiver:
309
+
310
+ ```bash
311
+ npx -y derphole@latest listen | tar -xpf - -C /restore/path
312
+ ```
313
+
314
+ `listen` prints a token on stderr. Copy that token to the sender over a channel you trust.
315
+
316
+ Sender:
317
+
318
+ ```bash
319
+ tar -cpf - /srv/data | npx -y derphole@latest pipe <token>
320
+ ```
321
+
322
+ This is still tar pipe. Difference: no public listener to expose, no SSH daemon required for the data path, no VPN to join, and no permanent mesh to set up. `derphole` starts with DERP if needed, then promotes the live transfer onto direct UDP when a faster direct path becomes available.
323
+
324
+ ## Development
325
+
326
+ ```bash
327
+ mise install
328
+ mise run install-githooks
329
+ mise run check
330
+ mise run build
331
+ ```
332
+
333
+ `mise run build` writes `dist/derphole` and `dist/derptun`.
334
+
335
+ ## Verification
336
+
337
+ Local smoke test:
338
+
339
+ ```bash
340
+ mise run smoke-local
341
+ ```
342
+
343
+ Remote smoke tests against a host you control:
344
+
345
+ ```bash
346
+ REMOTE_HOST=my-server.example.com mise run smoke-remote
347
+ REMOTE_HOST=my-server.example.com mise run smoke-remote-share
348
+ REMOTE_HOST=my-server.example.com mise run smoke-remote-derptun
349
+ REMOTE_HOST=my-server.example.com mise run promotion-1g
350
+ ```
351
+
352
+ ## Releases
353
+
354
+ - npm packages: `derphole`, `derptun`
355
+ - production channels: `derphole@latest`, `derptun@latest`
356
+ - development channels: `derphole@dev`, `derptun@dev`
357
+ - bootstrap runbook: [docs/releases/npm-bootstrap.md](docs/releases/npm-bootstrap.md)
358
+
359
+ ## What Is DERP?
360
+
361
+ 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.
362
+
363
+ 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.
364
+
365
+ In `derphole`, DERP has two jobs:
366
+
367
+ - 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
368
+ - fallback relay: carry encrypted session traffic when NAT traversal has not succeeded yet or when direct connectivity is unavailable
369
+
370
+ 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, `derphole` promotes the live session onto that direct path. DERP only forwards bytes; it does not get the session keys needed to decrypt the traffic.
package/bin/derptun.js ADDED
@@ -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" ? "derptun.exe" : "derptun";
26
+ const binaryPath = path.join(__dirname, "..", "vendor", triple, "derptun", 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, DERPTUN_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": "derptun",
3
+ "version": "0.0.0-dev.20260418025620.6a4cabb",
4
+ "license": "BSD-3-Clause",
5
+ "bin": {
6
+ "derptun": "bin/derptun.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/derphole.git"
29
+ }
30
+ }