@volter/tunnel 1.0.0 → 1.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/README.md +17 -0
- package/client/tunnel-client.ts +15 -3
- package/package.json +5 -1
- package/server/server.mjs +4 -2
package/README.md
CHANGED
|
@@ -79,6 +79,23 @@ npm install && node server.mjs
|
|
|
79
79
|
|
|
80
80
|
Client and relay must share the same `TUNNEL_SECRET`.
|
|
81
81
|
|
|
82
|
+
## Server — scalable relay (Cloudflare Workers + Durable Objects)
|
|
83
|
+
|
|
84
|
+
`server/` (the Fly relay) is a single stateful process and does not scale
|
|
85
|
+
horizontally. For large/global scale there is a drop-in alternative in
|
|
86
|
+
`server-cf/`: a Cloudflare **Worker** that routes by subdomain to **one Durable
|
|
87
|
+
Object per tunnelId**, which holds the client's control socket (hibernatable, so
|
|
88
|
+
idle tunnels are ~free). Same wire protocol and client — the only client-visible
|
|
89
|
+
change is that `createTunnel` appends `?id=<tunnelId>` to the control URL so the
|
|
90
|
+
Worker can pick the right DO (the Fly relay tolerates and ignores it).
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
cd server-cf
|
|
94
|
+
npm run dev # wrangler dev --local (real workerd)
|
|
95
|
+
npm test # vitest: real Worker+DO + createTunnel, no mocks
|
|
96
|
+
npm run deploy # wrangler deploy (needs a CF zone for the wildcard host)
|
|
97
|
+
```
|
|
98
|
+
|
|
82
99
|
## Layout
|
|
83
100
|
|
|
84
101
|
```
|
package/client/tunnel-client.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
1
2
|
/**
|
|
2
3
|
* WebSocket-based HTTP tunnel client.
|
|
3
4
|
*
|
|
@@ -13,7 +14,11 @@ import https from 'node:https';
|
|
|
13
14
|
import net from 'node:net';
|
|
14
15
|
import WebSocket from 'ws';
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Force HTTP/1.1 for the (TLS) control WebSocket — HTTP/2 breaks the Upgrade
|
|
19
|
+
* handshake. Only applies to wss:// (an https.Agent rejects a plain ws:// URL),
|
|
20
|
+
* so it's used conditionally; plain ws:// (e.g. a local relay) needs no agent.
|
|
21
|
+
*/
|
|
17
22
|
const http1Agent = new https.Agent({ ALPNProtocols: ['http/1.1'] });
|
|
18
23
|
|
|
19
24
|
/** Minimal logger interface for tunnel client — compatible with pino, console, etc. */
|
|
@@ -265,7 +270,14 @@ export function createTunnel({
|
|
|
265
270
|
logger,
|
|
266
271
|
}: TunnelOptions): Promise<TunnelHandle> {
|
|
267
272
|
const log = logger ?? defaultLogger;
|
|
268
|
-
|
|
273
|
+
// Include the tunnelId in the control URL so a routing relay (e.g. the
|
|
274
|
+
// Cloudflare Workers + Durable Objects relay) can pick the right backend at
|
|
275
|
+
// upgrade time, before the `register` message is sent. The Fly relay ignores
|
|
276
|
+
// the query param and reads tunnelId from `register`, so one client works
|
|
277
|
+
// against both servers.
|
|
278
|
+
const wsUrl =
|
|
279
|
+
`${host.replace(/^http/, 'ws')}/ws` +
|
|
280
|
+
(tunnelId ? `?id=${encodeURIComponent(tunnelId)}` : '');
|
|
269
281
|
|
|
270
282
|
// Resolved loopback address for this port (set before first connection)
|
|
271
283
|
let localHost = '127.0.0.1';
|
|
@@ -284,7 +296,7 @@ export function createTunnel({
|
|
|
284
296
|
): void {
|
|
285
297
|
if (closed) return;
|
|
286
298
|
|
|
287
|
-
const ws = new WebSocket(wsUrl, { agent: http1Agent });
|
|
299
|
+
const ws = new WebSocket(wsUrl, wsUrl.startsWith('wss:') ? { agent: http1Agent } : {});
|
|
288
300
|
let registered = false;
|
|
289
301
|
// Hoisted so every teardown path (a reconnect via the 'close' handler, or an
|
|
290
302
|
// explicit handle.close()) can clear it. It used to be declared inside the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/tunnel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Volter tunnel — WebSocket-based HTTP/WS reverse tunnel: a relay server plus a client connector library and CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "bun test ./test",
|
|
25
|
+
"test:cf": "cd server-cf && vitest run",
|
|
24
26
|
"tunnel": "bun run client/tunnel-client.ts",
|
|
25
27
|
"server": "node server/server.mjs"
|
|
26
28
|
},
|
|
@@ -36,8 +38,10 @@
|
|
|
36
38
|
}
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
41
|
+
"@types/jsonwebtoken": "^9.0.0",
|
|
39
42
|
"@types/node": "^22.0.0",
|
|
40
43
|
"@types/ws": "^8.5.14",
|
|
44
|
+
"jsonwebtoken": "^9.0.2",
|
|
41
45
|
"typescript": "^5.0.0"
|
|
42
46
|
}
|
|
43
47
|
}
|
package/server/server.mjs
CHANGED
|
@@ -422,8 +422,10 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
422
422
|
|
|
423
423
|
const tunnelId = getTunnelIdFromHost(req.headers.host);
|
|
424
424
|
|
|
425
|
-
// No subdomain + /ws path → control channel for tunnel clients
|
|
426
|
-
|
|
425
|
+
// No subdomain + /ws path → control channel for tunnel clients.
|
|
426
|
+
// Tolerate a query string (the client appends ?id=<tunnelId> so a routing
|
|
427
|
+
// relay can pick its backend; this server reads tunnelId from `register`).
|
|
428
|
+
if (!tunnelId && req.url && req.url.split('?')[0] === '/ws') {
|
|
427
429
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
428
430
|
wss.emit('connection', ws, req);
|
|
429
431
|
});
|