derptun 0.1.0

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,360 @@
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
+
12
+ `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.
13
+
14
+ `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).
15
+
16
+ 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).
17
+
18
+ It does **not** require:
19
+
20
+ - a Tailscale account
21
+ - a tailnet
22
+ - `tailscaled`
23
+ - a separate control plane to run yourself
24
+
25
+ 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.
26
+
27
+ ## Pick the Workflow
28
+
29
+ - Use `listen` and `pipe` for raw byte streams and shell pipelines.
30
+ - Use `send` and `receive` for text, files, directories, progress, and receive-code UX.
31
+ - Use `share` and `open` for temporary access to a local TCP service.
32
+ - Use `ssh invite` and `ssh accept` for SSH public key exchange.
33
+ - Use `derptun` for durable TCP tunnels with reusable, longer-lived tokens.
34
+
35
+ ## Quick Start
36
+
37
+ `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.
38
+
39
+ ### Stream a Raw File
40
+
41
+ On the receiving machine:
42
+
43
+ ```bash
44
+ npx -y derphole@latest listen > received.img
45
+ ```
46
+
47
+ `listen` prints a token to stderr so stdout stays clean for received bytes. Copy that token to the sending machine.
48
+
49
+ On the sending machine:
50
+
51
+ ```bash
52
+ cat ./disk.img | npx -y derphole@latest pipe <token>
53
+ ```
54
+
55
+ For quick text:
56
+
57
+ ```bash
58
+ printf 'hello\n' | npx -y derphole@latest pipe <token>
59
+ ```
60
+
61
+ ### Send with a Receive Code
62
+
63
+ On the sending machine:
64
+
65
+ ```bash
66
+ npx -y derphole@latest send ./photo.jpg
67
+ ```
68
+
69
+ `send` prints the command to run on the receiving machine:
70
+
71
+ ```bash
72
+ npx -y derphole@latest receive <code>
73
+ ```
74
+
75
+ For known-size file and directory transfers, `derphole` prints wormhole-shaped progress and rate output on stderr. Use `--hide-progress` for quiet output.
76
+
77
+ Text uses the same flow:
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
+ ### Exchange SSH Access
90
+
91
+ The host receiving access runs:
92
+
93
+ ```bash
94
+ npx -y derphole@latest ssh invite --user deploy
95
+ ```
96
+
97
+ The other side accepts with:
98
+
99
+ ```bash
100
+ npx -y derphole@latest ssh accept <token>
101
+ ```
102
+
103
+ ### Watch Progress with `pv`
104
+
105
+ `derphole` is plain stdin/stdout, so `pv` fits naturally in the pipeline.
106
+
107
+ Install `pv` if needed:
108
+
109
+ ```bash
110
+ brew install pv
111
+ sudo apt install -y pv
112
+ ```
113
+
114
+ On the receiving machine:
115
+
116
+ ```bash
117
+ npx -y derphole@latest listen | pv -brt > received.img
118
+ ```
119
+
120
+ On the sending machine:
121
+
122
+ ```bash
123
+ cat ./disk.img | pv -brt | npx -y derphole@latest pipe <token>
124
+ ```
125
+
126
+ 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).
127
+
128
+ ### Share a Local TCP Service
129
+
130
+ On the machine running the local web app or API:
131
+
132
+ ```bash
133
+ npx -y derphole@latest share 127.0.0.1:3000
134
+ ```
135
+
136
+ `share` prints a token to stderr. Copy that token to the machine that needs access.
137
+
138
+ On another machine, expose the shared service locally:
139
+
140
+ ```bash
141
+ npx -y derphole@latest open <token>
142
+ ```
143
+
144
+ `open` prints the local listening address to stderr.
145
+
146
+ Bind `open` to a specific local port:
147
+
148
+ ```bash
149
+ npx -y derphole@latest open <token> 127.0.0.1:8080
150
+ ```
151
+
152
+ ### Durable SSH Tunnels with `derptun`
153
+
154
+ `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.
155
+
156
+ On the target host:
157
+
158
+ ```bash
159
+ npx -y derptun@latest token --days 7 > alpha.token
160
+ npx -y derptun@latest serve --token "$(cat alpha.token)" --tcp 127.0.0.1:22
161
+ ```
162
+
163
+ On the client:
164
+
165
+ ```bash
166
+ npx -y derptun@latest open --token "$(cat alpha.token)" --listen 127.0.0.1:2222
167
+ ssh -p 2222 foo@127.0.0.1
168
+ ```
169
+
170
+ For SSH without a separate local listener, use `ProxyCommand`:
171
+
172
+ ```sshconfig
173
+ Host alpha-derptun
174
+ HostName alpha
175
+ User foo
176
+ ProxyCommand derptun connect --token ~/.config/derptun/alpha.token --stdio
177
+ ```
178
+
179
+ `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.
180
+
181
+ 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.
182
+
183
+ ### Useful Extras
184
+
185
+ Use the development channel for the latest commit from `main`:
186
+
187
+ ```bash
188
+ npx -y derphole@dev version
189
+ ```
190
+
191
+ 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`:
192
+
193
+ ```bash
194
+ npx -y derphole@latest --verbose listen
195
+ npx -y derphole@latest --verbose pipe <token>
196
+ npx -y derphole@latest --verbose send ./photo.jpg
197
+ ```
198
+
199
+ For transport details, see [Transport Model](#transport-model), [Behavior](#behavior), or [Security Model](#security-model).
200
+
201
+ ## Transport Model
202
+
203
+ High-level flow:
204
+
205
+ 1. `listen`, `share`, or `receive` creates an ephemeral session and prints an opaque bearer token or receive code.
206
+ 2. The token carries the session ID, expiry, DERP bootstrap hints, the listener's public peer identity, bearer secret, and allowed capability.
207
+ 3. `pipe`, `send`, or `open` uses that token to contact the listener through DERP and claim the session.
208
+ 4. The listener validates the claim, checks the requested capability, and returns current direct-path candidates.
209
+ 5. Both sides start on the first working path, including DERP relay if direct connectivity is not ready yet.
210
+ 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.
211
+
212
+ ### Data Plane Selection
213
+
214
+ DERP provides **rendezvous** and **relay fallback**. If the term is new, see [What Is DERP?](#what-is-derp):
215
+
216
+ - rendezvous: exchange initial claim, decision, and direct-path coordination messages without an account-backed control plane
217
+ - relay fallback: keep the session working when NAT traversal fails or direct connectivity is not ready yet
218
+
219
+ The data plane is selected per session:
220
+
221
+ - `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.
222
+ - `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.
223
+ - `send/receive` wraps the same one-shot stream with text, file, directory, and progress metadata.
224
+
225
+ Candidate discovery splits into two phases:
226
+
227
+ - fast local candidates first: advertise local socket/interface candidates and any cached port mapping immediately
228
+ - 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
229
+
230
+ This keeps startup latency low while still allowing relay-to-direct promotion.
231
+
232
+ ## How This Differs From Tailscale / WireGuard
233
+
234
+ 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.
235
+
236
+ `derphole` does something narrower. It creates session-scoped transport for a single transfer or one shared service:
237
+
238
+ - no WireGuard tunnel device
239
+ - no overlay network interface
240
+ - no persistent mesh control plane
241
+ - no need to route arbitrary traffic through a general encrypted network
242
+
243
+ 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).
244
+
245
+ 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.
246
+
247
+ ## Why It Is Fast
248
+
249
+ `derphole` gets performance from its transport design:
250
+
251
+ - DERP is for rendezvous and relay fallback, not the preferred steady-state data plane.
252
+ - Sessions can start relayed immediately, then promote in place to direct without restarting the transfer.
253
+ - `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.
254
+ - 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.
255
+ - `share/open` keeps QUIC stream multiplexing for service sharing, where many independent TCP streams need one claimed session.
256
+ - 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.
257
+
258
+ 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.
259
+
260
+ ## Security Model
261
+
262
+ The session token is a **bearer capability**. Anyone with the token can claim the session until it expires, so share it over a trusted channel. Tokens expire after one hour.
263
+
264
+ DERP relays do **not** get the secret material needed to read or impersonate the session:
265
+
266
+ - 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.
267
+ - On `share/open`, stream traffic uses authenticated QUIC streams for the claimed session.
268
+ - If packets are relayed through DERP, DERP only forwards encrypted session bytes.
269
+
270
+ Important security property: `derphole` does not trade speed for plaintext shortcuts:
271
+
272
+ - the token authorizes the session, but does not turn DERP into a trusted decrypting proxy
273
+ - direct UDP data packets are encrypted and authenticated per session
274
+ - QUIC stream-mode peers are pinned to the expected public identity from the token
275
+ - DERP forwards encrypted traffic but does not have the keys required to decrypt or impersonate the session
276
+
277
+ Simple rule: token possession authorizes the session, but intermediaries that only see DERP traffic do not have the keys needed to decrypt it.
278
+
279
+ ## Behavior
280
+
281
+ 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.
282
+
283
+ ## Use Cases
284
+
285
+ - cross-host transfer with no account setup
286
+ - NAT-heavy networks where direct connectivity may or may not work
287
+ - quick sharing of local web apps, APIs, and admin interfaces
288
+ - `npx` use without manual install
289
+
290
+ ## Real-World Example: Tar Pipe Over Internet
291
+
292
+ 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).
293
+
294
+ 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.
295
+
296
+ `derphole` keeps the same streaming shape, but removes the open-port requirement.
297
+
298
+ Receiver:
299
+
300
+ ```bash
301
+ npx -y derphole@latest listen | tar -xpf - -C /restore/path
302
+ ```
303
+
304
+ `listen` prints a token on stderr. Copy that token to the sender over a channel you trust.
305
+
306
+ Sender:
307
+
308
+ ```bash
309
+ tar -cpf - /srv/data | npx -y derphole@latest pipe <token>
310
+ ```
311
+
312
+ 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.
313
+
314
+ ## Development
315
+
316
+ ```bash
317
+ mise install
318
+ mise run install-githooks
319
+ mise run check
320
+ mise run build
321
+ ```
322
+
323
+ `mise run build` writes `dist/derphole` and `dist/derptun`.
324
+
325
+ ## Verification
326
+
327
+ Local smoke test:
328
+
329
+ ```bash
330
+ mise run smoke-local
331
+ ```
332
+
333
+ Remote smoke tests against a host you control:
334
+
335
+ ```bash
336
+ REMOTE_HOST=my-server.example.com mise run smoke-remote
337
+ REMOTE_HOST=my-server.example.com mise run smoke-remote-share
338
+ REMOTE_HOST=my-server.example.com mise run smoke-remote-derptun
339
+ REMOTE_HOST=my-server.example.com mise run promotion-1g
340
+ ```
341
+
342
+ ## Releases
343
+
344
+ - npm packages: `derphole`, `derptun`
345
+ - production channels: `derphole@latest`, `derptun@latest`
346
+ - development channels: `derphole@dev`, `derptun@dev`
347
+ - bootstrap runbook: [docs/releases/npm-bootstrap.md](docs/releases/npm-bootstrap.md)
348
+
349
+ ## What Is DERP?
350
+
351
+ 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.
352
+
353
+ 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.
354
+
355
+ In `derphole`, DERP has two jobs:
356
+
357
+ - 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
358
+ - fallback relay: carry encrypted session traffic when NAT traversal has not succeeded yet or when direct connectivity is unavailable
359
+
360
+ 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.1.0",
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
+ }