@volter/tunnel 1.0.1 → 1.1.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/README.md +17 -0
- package/client/tunnel-client.ts +20 -4
- 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
|
@@ -14,7 +14,11 @@ import https from 'node:https';
|
|
|
14
14
|
import net from 'node:net';
|
|
15
15
|
import WebSocket from 'ws';
|
|
16
16
|
|
|
17
|
-
/**
|
|
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
|
+
*/
|
|
18
22
|
const http1Agent = new https.Agent({ ALPNProtocols: ['http/1.1'] });
|
|
19
23
|
|
|
20
24
|
/** Minimal logger interface for tunnel client — compatible with pino, console, etc. */
|
|
@@ -266,7 +270,14 @@ export function createTunnel({
|
|
|
266
270
|
logger,
|
|
267
271
|
}: TunnelOptions): Promise<TunnelHandle> {
|
|
268
272
|
const log = logger ?? defaultLogger;
|
|
269
|
-
|
|
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)}` : '');
|
|
270
281
|
|
|
271
282
|
// Resolved loopback address for this port (set before first connection)
|
|
272
283
|
let localHost = '127.0.0.1';
|
|
@@ -285,7 +296,7 @@ export function createTunnel({
|
|
|
285
296
|
): void {
|
|
286
297
|
if (closed) return;
|
|
287
298
|
|
|
288
|
-
const ws = new WebSocket(wsUrl, { agent: http1Agent });
|
|
299
|
+
const ws = new WebSocket(wsUrl, wsUrl.startsWith('wss:') ? { agent: http1Agent } : {});
|
|
289
300
|
let registered = false;
|
|
290
301
|
// Hoisted so every teardown path (a reconnect via the 'close' handler, or an
|
|
291
302
|
// explicit handle.close()) can clear it. It used to be declared inside the
|
|
@@ -834,8 +845,13 @@ if (import.meta.main) {
|
|
|
834
845
|
process.exit(1);
|
|
835
846
|
}
|
|
836
847
|
|
|
848
|
+
// Default to the Cloudflare Workers + Durable Objects relay. The old Fly relay
|
|
849
|
+
// (vgit-tunnels.volterapp.com) returns HTTP 200 instead of 101 on HTTP/2
|
|
850
|
+
// WebSocket upgrades, which breaks browser-based flows (e.g. QA proofs).
|
|
837
851
|
const host =
|
|
838
|
-
flag('host') ||
|
|
852
|
+
flag('host') ||
|
|
853
|
+
process.env.TUNNEL_SERVER_URL ||
|
|
854
|
+
'https://volter-tunnel.aaron-0ed.workers.dev';
|
|
839
855
|
const secret = process.env.TUNNEL_SECRET;
|
|
840
856
|
const tunnelId = flag('tunnel-id');
|
|
841
857
|
const authNotRequired = args.includes('--auth-not-required');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/tunnel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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
|
});
|