derpssh 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,338 @@
1
+ # derphole
2
+
3
+ `derphole` is a standalone CLI for session-scoped byte transfer and temporary local TCP service sharing. Use it for one-shot transfers, receive-code flows, and short-lived service sharing.
4
+
5
+ [`derptun`](#long-lived-tcp-tunnels) is its companion for long-lived TCP tunnels. Use it when a tunnel needs stable tokens, restartable endpoints, and repeated client reconnects.
6
+
7
+ [`derpssh`](#share-a-terminal) is its companion for interactive terminal sharing. Use it when two people need one shared PTY with host approval and no open ports.
8
+
9
+ `derphole` supports:
10
+
11
+ - raw byte streams with `listen` and `pipe`
12
+ - text, file, and directory transfer with `send` and `receive`
13
+ - local TCP service sharing with `share` and `open`
14
+ - SSH access exchange with `ssh invite` and `ssh accept`
15
+
16
+ Both tools use the public Tailscale [DERP](#what-is-derp) relay network for rendezvous and fallback, then promote live traffic to direct encrypted UDP when possible. Payload bytes stay end-to-end encrypted on relay fallback, direct UDP, and authenticated QUIC stream paths; DERP sees routing metadata, not contents. They are **not** affiliated with Tailscale and do **not** use `tailscaled`.
17
+
18
+ Neither tool is a WireGuard overlay or VPN. `derphole` handles one token, one session, one transfer or shared service. `derptun` handles one long-lived tunnel. See [Transport Model](#transport-model), [Why It Is Fast](#why-it-is-fast), and [Security Model](#security-model).
19
+
20
+ Neither tool requires:
21
+
22
+ - a Tailscale account
23
+ - a tailnet
24
+ - `tailscaled`
25
+ - a separate control plane to run yourself
26
+
27
+ Session tokens carry authorization. Public sessions fetch the DERP map at runtime so both sides can find relay and bootstrap nodes. See [Security Model](#security-model) for token and relay details.
28
+
29
+ ## Pick the Workflow
30
+
31
+ - Use `listen` and `pipe` for raw byte streams and shell pipelines.
32
+ - Use `send` and `receive` for text, files, directories, progress, and receive-code UX.
33
+ - Use `share` and `open` for temporary access to a local TCP service.
34
+ - Use `ssh invite` and `ssh accept` for SSH public key exchange.
35
+ - Use [`derpssh`](#share-a-terminal) for approved terminal sharing.
36
+ - Use [`derptun`](#long-lived-tcp-tunnels) for long-lived TCP tunnels with reusable tokens.
37
+
38
+ ## Quick Start
39
+
40
+ `listen` receives bytes and prints a token. `pipe` sends stdin into that token. `share` and `open` do the same for local TCP services. Use [`derptun`](#long-lived-tcp-tunnels) for reusable, longer-lived tunnels.
41
+
42
+ ### Stream a Raw File
43
+
44
+ Receiver:
45
+
46
+ ```bash
47
+ npx -y derphole@latest listen > received.img
48
+ ```
49
+
50
+ `listen` prints a token to stderr, keeping stdout clean. Copy the token to the sender.
51
+
52
+ Sender:
53
+
54
+ ```bash
55
+ cat ./disk.img | npx -y derphole@latest pipe <token>
56
+ ```
57
+
58
+ For quick text:
59
+
60
+ ```bash
61
+ printf 'hello\n' | npx -y derphole@latest pipe <token>
62
+ ```
63
+
64
+ ### Send with a Receive Code
65
+
66
+ Sender:
67
+
68
+ ```bash
69
+ npx -y derphole@latest send ./photo.jpg
70
+ ```
71
+
72
+ `send` prints the receiver command:
73
+
74
+ ```bash
75
+ npx -y derphole@latest receive <code>
76
+ ```
77
+
78
+ Known-size files and directories show progress on stderr. Use `--hide-progress` for quiet output.
79
+
80
+ Text uses the same flow:
81
+
82
+ ```bash
83
+ npx -y derphole@latest send hello
84
+ ```
85
+
86
+ Directories stream as tar and re-materialize on the receiver:
87
+
88
+ ```bash
89
+ npx -y derphole@latest send ./project-dir
90
+ ```
91
+
92
+ ### Exchange SSH Access
93
+
94
+ Host granting access:
95
+
96
+ ```bash
97
+ npx -y derphole@latest ssh invite --user deploy
98
+ ```
99
+
100
+ Client:
101
+
102
+ ```bash
103
+ npx -y derphole@latest ssh accept <token>
104
+ ```
105
+
106
+ ### Share a Local TCP Service
107
+
108
+ Service host:
109
+
110
+ ```bash
111
+ npx -y derphole@latest share 127.0.0.1:3000
112
+ ```
113
+
114
+ `share` prints a token to stderr. Copy it to the client machine.
115
+
116
+ Client:
117
+
118
+ ```bash
119
+ npx -y derphole@latest open <token>
120
+ ```
121
+
122
+ `open` prints the local listening address to stderr.
123
+
124
+ Bind `open` to a specific local port:
125
+
126
+ ```bash
127
+ npx -y derphole@latest open <token> 127.0.0.1:8080
128
+ ```
129
+
130
+ ### Share a Terminal
131
+
132
+ Host:
133
+
134
+ ```bash
135
+ npx -y derpssh@latest share
136
+ ```
137
+
138
+ `share` prints a connect command. Send it to the guest:
139
+
140
+ ```bash
141
+ npx -y derpssh@latest connect <invite>
142
+ ```
143
+
144
+ The host approves the guest as read-only or read/write. The session uses the derptun transport path, so neither side needs an inbound port.
145
+
146
+ ### Long-Lived TCP Tunnels
147
+
148
+ `derptun` is the long-lived TCP tunnel companion to `derphole`. It uses stable tokens, survives restarts on either side, and lets one client reconnect many times without opening ports on `vps-server`. It fits SSH well.
149
+
150
+ On `vps-server`:
151
+
152
+ ```bash
153
+ npx -y derptun@latest token server > server.dts
154
+ npx -y derptun@latest token client --token-file server.dts > client.dtc
155
+ npx -y derptun@latest serve --token-file server.dts --tcp 127.0.0.1:22
156
+ ```
157
+
158
+ Copy only `client.dtc` to `alice-laptop`.
159
+
160
+ On `alice-laptop`:
161
+
162
+ ```bash
163
+ npx -y derptun@latest open --token-file client.dtc --listen 127.0.0.1:2222
164
+ ssh -p 2222 user@127.0.0.1
165
+ ```
166
+
167
+ For SSH without a separate local listener, use `ProxyCommand`:
168
+
169
+ ```bash
170
+ ssh -o ProxyCommand='npx -y derptun@latest connect --token-file ./client.dtc --stdio' foo@127.0.0.1
171
+ ```
172
+
173
+ The server token is serving authority. Keep it on the serving machine or in its secret manager. The client token can connect until expiry, but cannot serve or mint tokens.
174
+
175
+ Server tokens default to 180 days. Client tokens default to 90 days and cannot outlive their server token. Set a relative lifetime with `--days`, or use an absolute expiry:
176
+
177
+ ```bash
178
+ npx -y derptun@latest token server --expires 2026-05-01T00:00:00Z > server.dts
179
+ npx -y derptun@latest token client --token-file server.dts --expires 2026-04-25T00:00:00Z > client.dtc
180
+ ```
181
+
182
+ Use `--token TOKEN` for inline one-off commands. Prefer `--token-file PATH` for durable tokens. `--token-stdin` reads the token from the first stdin line.
183
+
184
+ `derptun` is TCP-only for now. UDP forwarding is planned for use cases like Minecraft Bedrock servers.
185
+
186
+ ### Useful Extras
187
+
188
+ Use the development channel for the latest commit from `main`:
189
+
190
+ ```bash
191
+ npx -y derphole@dev version
192
+ npx -y derptun@dev version
193
+ npx -y derpssh@dev version
194
+ ```
195
+
196
+ Default output stays quiet: tokens, bind addresses, receive commands, and progress only. Use `--hide-progress` to suppress progress, or `--verbose` to see transitions like `connected-relay` and `connected-direct`:
197
+
198
+ ```bash
199
+ npx -y derphole@latest --verbose listen
200
+ npx -y derphole@latest --verbose pipe <token>
201
+ npx -y derphole@latest --verbose send ./photo.jpg
202
+ ```
203
+
204
+ For transport details, see [Transport Model](#transport-model), [Behavior](#behavior), and [Security Model](#security-model).
205
+
206
+ ## Transport Model
207
+
208
+ Flow:
209
+
210
+ 1. `listen`, `share`, or `receive` creates a session and prints an opaque bearer token or receive code.
211
+ 2. The token carries session ID, expiry, DERP bootstrap hints, listener public identity, bearer secret, and allowed capability.
212
+ 3. `pipe`, `send`, or `open` uses that token to contact the listener through DERP and claim the session.
213
+ 4. The listener validates the claim, checks the requested capability, and returns current direct-path candidates.
214
+ 5. Both sides start on the first working path, including DERP relay if needed.
215
+ 6. Both sides keep probing for a better direct path. Successful direct paths upgrade the live session in place.
216
+
217
+ ### Data Plane Selection
218
+
219
+ DERP provides **rendezvous** and **relay fallback**. See [What Is DERP?](#what-is-derp):
220
+
221
+ - rendezvous: exchange claim, decision, and direct-path coordination messages without an account-backed control plane
222
+ - relay fallback: keep the session working when NAT traversal fails or direct connectivity is not ready
223
+
224
+ The data plane is selected per session:
225
+
226
+ - `share/open` uses multiplexed QUIC streams over `derphole`'s relay/direct UDP transport. One claimed session can carry many TCP connections to the shared service.
227
+ - `derptun` uses a stable tunnel token and the same transport for reconnectable TCP streams. It is built for longer-lived access, such as SSH to a host behind NAT.
228
+ - `derpssh` uses the derptun app mux for approved terminal streams and side-channel control.
229
+ - `listen/pipe` uses a one-shot byte stream. It coordinates through DERP, promotes to rate-adaptive direct UDP when traversal succeeds, and stays on encrypted relay fallback when direct paths fail.
230
+ - `send/receive` wraps the same one-shot stream with text, file, directory, and progress metadata.
231
+
232
+ Candidate discovery splits into two phases:
233
+
234
+ - fast local candidates first: advertise local sockets, interfaces, and cached port mappings immediately
235
+ - background traversal discovery: run STUN and UPnP / NAT-PMP / PCP refresh, then send updated candidates and `call-me-maybe` probes
236
+
237
+ This keeps startup latency low while preserving relay-to-direct promotion.
238
+
239
+ ## How This Differs From Tailscale / WireGuard
240
+
241
+ Tailscale uses WireGuard for a secure general-purpose network: durable machine connectivity, private addresses, ACLs, subnet routing, exit nodes, and long-lived overlays.
242
+
243
+ `derphole` is narrower. It creates session-scoped transport for one transfer or one shared service:
244
+
245
+ - no WireGuard tunnel device
246
+ - no overlay network interface
247
+ - no persistent mesh control plane
248
+ - no need to route arbitrary traffic through a general encrypted network
249
+
250
+ Instead, `derphole` authorizes one session with a bearer token, uses DERP to connect peers immediately, then promotes onto the best direct path it can establish. See [Transport Model](#transport-model) and [Security Model](#security-model).
251
+
252
+ For `listen/pipe`, `send/receive`, and `share/open`, this can beat routing the same traffic through a WireGuard-based overlay because `derphole` optimizes one active session. See [Why It Is Fast](#why-it-is-fast).
253
+
254
+ ## Why It Is Fast
255
+
256
+ Performance comes from transport shape:
257
+
258
+ - DERP handles rendezvous and fallback, not preferred steady-state data.
259
+ - Sessions can start relayed, then promote in place to direct without restarting.
260
+ - `listen/pipe` and `send/receive` can scale across direct UDP lanes, run path-rate probes, then use paced sending, adaptive rate control, and targeted replay/repair. Fast links can run near WAN ceiling without forcing slower links into the same send rate.
261
+ - Direct UDP payloads use AEAD with a per-session key derived from the bearer secret. Headers stay visible for sequencing and repair; user bytes stay encrypted and authenticated.
262
+ - `share/open` keeps QUIC stream multiplexing for service sharing, where many independent TCP streams need one claimed session.
263
+ - Candidate discovery starts with local interfaces and cached mappings, then refines in the background with STUN and port mapping refresh.
264
+
265
+ Result: move bytes early, keep relay fallback, and shift live sessions to direct paths when ready.
266
+
267
+ ## Security Model
268
+
269
+ Tokens are **bearer capabilities**. Anyone with a token can claim the matching session or tunnel until expiry, so share tokens over a trusted channel. `derphole` session tokens expire after one hour. `derptun` server tokens default to 180 days and can mint shorter-lived client tokens. Client tokens default to 90 days and cannot serve.
270
+
271
+ Payload bytes are always end-to-end encrypted between token holders. Session and tunnel encryption is pinned to token-derived identity, so DERP relays do **not** get keys needed to read or impersonate sessions. DERP can see routing metadata and packet timing, but not plaintext user payload bytes:
272
+
273
+ - On `listen/pipe` and `send/receive`, direct UDP and relay fallback both encrypt and authenticate user payloads with session AEAD derived from the bearer secret.
274
+ - Relay-prefix startup frames leave frame kind and byte offsets visible for flow control, but encrypt user payload bytes.
275
+ - On `share/open`, stream traffic uses authenticated QUIC streams for the claimed session.
276
+ - On `derptun`, stream traffic uses authenticated QUIC streams pinned to the stable tunnel identity in the token.
277
+ - On `derpssh`, terminal streams use authenticated QUIC streams pinned to the invite identity.
278
+
279
+ Simple rule: token possession authorizes the session. Relays move packets; they do not hold decrypt keys for user payloads.
280
+
281
+ ## Behavior
282
+
283
+ Sessions can start on DERP relay and later promote to direct paths without restarting. By default, CLI output stays minimal. Use `--verbose` for path changes, NAT traversal state, and direct-path tuning.
284
+
285
+ ## Use Cases
286
+
287
+ - cross-host transfer with no account setup
288
+ - NAT-heavy networks where direct connectivity may or may not work
289
+ - quick sharing of local web apps, APIs, and admin interfaces
290
+ - `npx` use without manual install
291
+
292
+ ## Development
293
+
294
+ ```bash
295
+ mise install
296
+ mise run install-githooks
297
+ mise run check
298
+ mise run build
299
+ ```
300
+
301
+ `mise run build` writes `dist/derphole`, `dist/derptun`, and `dist/derpssh`.
302
+
303
+ ## Verification
304
+
305
+ Local smoke test:
306
+
307
+ ```bash
308
+ mise run smoke-local
309
+ ```
310
+
311
+ Remote smoke tests against a host you control:
312
+
313
+ ```bash
314
+ REMOTE_HOST=my-server.example.com mise run smoke-remote
315
+ REMOTE_HOST=my-server.example.com mise run smoke-remote-share
316
+ REMOTE_HOST=my-server.example.com mise run smoke-remote-derptun
317
+ REMOTE_HOST=my-server.example.com mise run smoke-remote-derpssh
318
+ REMOTE_HOST=my-server.example.com mise run promotion-1g
319
+ ```
320
+
321
+ ## Releases
322
+
323
+ - npm packages: `derphole`, `derptun`, `derpssh`
324
+ - production channels: `derphole@latest`, `derptun@latest`, `derpssh@latest`
325
+ - development channels: `derphole@dev`, `derptun@dev`, `derpssh@dev`
326
+
327
+ ## What Is DERP?
328
+
329
+ DERP stands for **Designated Encrypted Relay for Packets**. It is a globally reachable relay network that both peers can use when they cannot yet talk directly.
330
+
331
+ DERP was built by Tailscale for the Tailscale networking stack. The public Tailscale-operated DERP network is reachable without running your own relays. Headscale, the open-source Tailscale control server, can also serve DERP maps and DERP servers.
332
+
333
+ In `derphole`, DERP has two jobs:
334
+
335
+ - rendezvous: carry claim, decision, and direct-path coordination messages without a separate account-backed control plane
336
+ - fallback relay: carry encrypted session traffic when NAT traversal has not succeeded or direct connectivity is unavailable
337
+
338
+ DERP is not the preferred steady-state path. It starts the session and keeps it working. If direct UDP becomes available, `derphole` promotes the live session. DERP forwards bytes; it does not get session decrypt keys.
package/bin/derpssh.js ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) 2026 Shayne All rights reserved.
4
+ * Use of this source code is governed by a BSD-style
5
+ * license that can be found in the LICENSE file.
6
+ */
7
+
8
+ import { spawn } from "node:child_process";
9
+ import { existsSync } from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ const triples = new Map([
18
+ ["linux:x64", "x86_64-unknown-linux-musl"],
19
+ ["linux:arm64", "aarch64-unknown-linux-musl"],
20
+ ["darwin:x64", "x86_64-apple-darwin"],
21
+ ["darwin:arm64", "aarch64-apple-darwin"]
22
+ ]);
23
+
24
+ const triple = triples.get(`${process.platform}:${process.arch}`);
25
+ if (!triple) {
26
+ console.error(`Unsupported platform: ${process.platform} (${process.arch})`);
27
+ process.exit(1);
28
+ }
29
+
30
+ const binaryName = process.platform === "win32" ? "derpssh.exe" : "derpssh";
31
+ const binaryPath = path.join(__dirname, "..", "vendor", triple, "derpssh", binaryName);
32
+ if (!existsSync(binaryPath)) {
33
+ console.error(`Missing vendored binary: ${binaryPath}`);
34
+ process.exit(1);
35
+ }
36
+
37
+ const child = spawn(binaryPath, process.argv.slice(2), {
38
+ stdio: "inherit",
39
+ env: { ...process.env, DERPSSH_MANAGED_BY_NPM: "1" }
40
+ });
41
+
42
+ child.on("error", (err) => {
43
+ const reason = err instanceof Error ? err.message : String(err);
44
+ console.error(`Failed to launch vendored binary: ${reason}`);
45
+ process.exit(1);
46
+ });
47
+
48
+ ["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => {
49
+ process.on(sig, () => {
50
+ if (!child.killed) {
51
+ child.kill(sig);
52
+ }
53
+ });
54
+ });
55
+
56
+ const result = await new Promise((resolve) => {
57
+ child.on("exit", (code, signal) => {
58
+ if (signal) {
59
+ resolve({ signal });
60
+ return;
61
+ }
62
+ resolve({ code: code ?? 1 });
63
+ });
64
+ });
65
+
66
+ if (result.signal) {
67
+ const signalNumber = os.constants.signals[result.signal];
68
+ process.exit(typeof signalNumber === "number" ? 128 + signalNumber : 1);
69
+ } else {
70
+ process.exit(result.code);
71
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "derpssh",
3
+ "version": "0.1.0",
4
+ "license": "BSD-3-Clause",
5
+ "bin": {
6
+ "derpssh": "bin/derpssh.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
+ }