cli-tunnel 1.2.0-beta.1 → 1.2.0-beta.2
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 +48 -5
- package/dist/index.js +86 -20
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -77,12 +77,55 @@ cli-tunnel copilot
|
|
|
77
77
|
|
|
78
78
|
## Security
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
cli-tunnel uses a layered security model:
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
**Network layer** — Microsoft Dev Tunnels are private by default. Only the Microsoft or GitHub account that created the tunnel can connect. TLS encryption is handled by Microsoft's relay infrastructure. No inbound ports are opened on your machine.
|
|
83
|
+
|
|
84
|
+
**Session authentication** — Each session generates a unique token (cryptographic random UUID). All HTTP API and WebSocket connections require this token. The token is embedded in the URL you receive at startup — anyone without it cannot connect.
|
|
85
|
+
|
|
86
|
+
**WebSocket auth** — cli-tunnel uses a ticket-based handshake: the browser exchanges the session token for a single-use, short-lived ticket (60 seconds) to establish the WebSocket connection. This avoids keeping the long-lived token in WebSocket upgrade logs.
|
|
87
|
+
|
|
88
|
+
**Input validation** — Only structured JSON messages are accepted over WebSocket. Raw text is rejected and logged. Terminal resize commands are bounds-checked to prevent abuse.
|
|
89
|
+
|
|
90
|
+
**Environment isolation** — The child process receives a filtered set of environment variables (an allowlist of ~40 safe variables like PATH, HOME, TERM). Sensitive variables and NODE_OPTIONS are excluded to prevent code injection.
|
|
91
|
+
|
|
92
|
+
**Audit logging** — All remote keyboard input is logged to `~/.cli-tunnel/audit/` in JSONL format with timestamps and source addresses. Secrets are automatically redacted from audit entries.
|
|
93
|
+
|
|
94
|
+
**Connection limits** — Maximum 5 concurrent WebSocket connections. Sessions expire after 24 hours.
|
|
95
|
+
|
|
96
|
+
## Terminal Size Behavior
|
|
97
|
+
|
|
98
|
+
cli-tunnel uses a single PTY (pseudo-terminal) shared between your local terminal and all remote viewers. When a phone or tablet connects, the PTY resizes to match the remote device's screen dimensions. This ensures the CLI app renders correctly on the device you're actively using to interact with it.
|
|
99
|
+
|
|
100
|
+
Because the PTY can only have one size at a time, the local terminal on your machine will reflect the remote device's dimensions while it's connected. This is by design — cli-tunnel prioritizes the remote viewing experience since the primary use case is controlling your CLI from another device.
|
|
101
|
+
|
|
102
|
+
**Tips for the best experience:**
|
|
103
|
+
- Rotate your phone to landscape for a wider terminal
|
|
104
|
+
- Use the key bar (↑↓←→ Tab Enter Esc Ctrl+C) at the bottom for navigation
|
|
105
|
+
- If multiple devices connect, the last one to resize wins
|
|
106
|
+
|
|
107
|
+
## FAQ
|
|
108
|
+
|
|
109
|
+
**Can multiple devices connect to the same session?**
|
|
110
|
+
Yes, up to 5 devices simultaneously. All viewers see the same terminal output in real time. Input from any device goes to the same CLI session.
|
|
111
|
+
|
|
112
|
+
**What happens if my phone disconnects?**
|
|
113
|
+
The CLI session keeps running on your machine. When you reconnect, you'll see live output from that point forward. Use `--replay` to enable history replay so reconnecting devices catch up on what they missed.
|
|
114
|
+
|
|
115
|
+
**Does cli-tunnel work with any CLI app?**
|
|
116
|
+
Yes. Any command that runs in a terminal works — copilot, vim, htop, python, ssh, k9s, node, and more. cli-tunnel doesn't interpret the command's output; it streams raw terminal bytes.
|
|
117
|
+
|
|
118
|
+
**Is there a central server?**
|
|
119
|
+
No. cli-tunnel runs entirely on your machine. Microsoft Dev Tunnels provides the relay infrastructure, but no third-party server sees your terminal content.
|
|
120
|
+
|
|
121
|
+
**What about the anti-phishing page?**
|
|
122
|
+
The first time you open a devtunnel URL, Microsoft shows an interstitial warning page. This is a devtunnel security feature — it confirms you trust the tunnel. You only see it once per tunnel.
|
|
123
|
+
|
|
124
|
+
**Does the tool work without devtunnel?**
|
|
125
|
+
Yes. Use `--local` to skip tunnel creation. The terminal is available at `http://127.0.0.1:<port>` on your local network only.
|
|
126
|
+
|
|
127
|
+
**What's hub mode?**
|
|
128
|
+
Run `cli-tunnel` with no command to start hub mode — a sessions dashboard that shows all active cli-tunnel sessions on your machine. Tap any online session to connect to it.
|
|
86
129
|
|
|
87
130
|
## How It's Built
|
|
88
131
|
|
package/dist/index.js
CHANGED
|
@@ -20,8 +20,15 @@ import crypto from 'node:crypto';
|
|
|
20
20
|
import { execSync, execFileSync, spawn } from 'node:child_process';
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import http from 'node:http';
|
|
23
|
+
import readline from 'node:readline';
|
|
23
24
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
24
25
|
import os from 'node:os';
|
|
26
|
+
function askUser(question) {
|
|
27
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
rl.question(question, (answer) => { rl.close(); resolve(answer.trim().toLowerCase()); });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
25
32
|
const BOLD = '\x1b[1m';
|
|
26
33
|
const RESET = '\x1b[0m';
|
|
27
34
|
const DIM = '\x1b[2m';
|
|
@@ -421,35 +428,73 @@ async function main() {
|
|
|
421
428
|
}
|
|
422
429
|
catch {
|
|
423
430
|
console.log(`\n ${YELLOW}⚠ devtunnel CLI not found!${RESET}\n`);
|
|
424
|
-
|
|
431
|
+
let installCmd = '';
|
|
425
432
|
if (process.platform === 'win32') {
|
|
426
|
-
|
|
433
|
+
installCmd = 'winget install Microsoft.devtunnel';
|
|
427
434
|
}
|
|
428
435
|
else if (process.platform === 'darwin') {
|
|
429
|
-
|
|
436
|
+
installCmd = 'brew install --cask devtunnel';
|
|
430
437
|
}
|
|
431
438
|
else {
|
|
432
|
-
|
|
439
|
+
installCmd = 'curl -sL https://aka.ms/DevTunnelCliInstall | bash';
|
|
433
440
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
441
|
+
const answer = await askUser(` Would you like to install it now? (${GREEN}${installCmd}${RESET}) [Y/n] `);
|
|
442
|
+
if (answer === '' || answer === 'y' || answer === 'yes') {
|
|
443
|
+
console.log(`\n ${DIM}Installing devtunnel...${RESET}\n`);
|
|
444
|
+
try {
|
|
445
|
+
const installParts = installCmd.split(' ');
|
|
446
|
+
const installProc = spawn(installParts[0], installParts.slice(1), { stdio: 'inherit', shell: process.platform !== 'win32' && installCmd.includes('|') });
|
|
447
|
+
await new Promise((resolve, reject) => {
|
|
448
|
+
installProc.on('close', (code) => code === 0 ? resolve() : reject(new Error(`Install exited with code ${code}`)));
|
|
449
|
+
installProc.on('error', reject);
|
|
450
|
+
});
|
|
451
|
+
// Verify installation
|
|
452
|
+
execFileSync('devtunnel', ['--version'], { stdio: 'pipe' });
|
|
453
|
+
console.log(`\n ${GREEN}✓${RESET} devtunnel installed successfully!\n`);
|
|
454
|
+
devtunnelInstalled = true;
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
console.log(`\n ${YELLOW}⚠${RESET} Installation failed: ${err.message}`);
|
|
458
|
+
console.log(` ${DIM}You can install it manually: ${installCmd}${RESET}\n`);
|
|
459
|
+
console.log(` ${DIM}Continuing without tunnel (local only)...${RESET}\n`);
|
|
445
460
|
}
|
|
446
461
|
}
|
|
447
|
-
|
|
448
|
-
console.log(`\n ${
|
|
449
|
-
console.log(` Run this once to log in:\n`);
|
|
450
|
-
console.log(` ${GREEN}devtunnel user login${RESET}\n`);
|
|
462
|
+
else {
|
|
463
|
+
console.log(`\n ${DIM}More info: https://aka.ms/devtunnels/doc${RESET}`);
|
|
451
464
|
console.log(` ${DIM}Continuing without tunnel (local only)...${RESET}\n`);
|
|
452
|
-
|
|
465
|
+
}
|
|
466
|
+
// If just installed, check login
|
|
467
|
+
if (devtunnelInstalled) {
|
|
468
|
+
try {
|
|
469
|
+
const userInfo = execFileSync('devtunnel', ['user', 'show'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
470
|
+
if (userInfo.includes('not logged in') || userInfo.includes('No user')) {
|
|
471
|
+
throw new Error('not logged in');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
console.log(` ${YELLOW}⚠ devtunnel not authenticated.${RESET}\n`);
|
|
476
|
+
const loginAnswer = await askUser(` Would you like to log in now? [Y/n] `);
|
|
477
|
+
if (loginAnswer === '' || loginAnswer === 'y' || loginAnswer === 'yes') {
|
|
478
|
+
try {
|
|
479
|
+
const loginProc = spawn('devtunnel', ['user', 'login'], { stdio: 'inherit' });
|
|
480
|
+
await new Promise((resolve, reject) => {
|
|
481
|
+
loginProc.on('close', (code) => code === 0 ? resolve() : reject(new Error(`Login exited with code ${code}`)));
|
|
482
|
+
loginProc.on('error', reject);
|
|
483
|
+
});
|
|
484
|
+
console.log(`\n ${GREEN}✓${RESET} Logged in successfully!\n`);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
console.log(`\n ${YELLOW}⚠${RESET} Login failed. Run manually: ${GREEN}devtunnel user login${RESET}\n`);
|
|
488
|
+
console.log(` ${DIM}Continuing without tunnel (local only)...${RESET}\n`);
|
|
489
|
+
devtunnelInstalled = false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
console.log(`\n ${DIM}Run this once to log in: ${GREEN}devtunnel user login${RESET}`);
|
|
494
|
+
console.log(` ${DIM}Continuing without tunnel (local only)...${RESET}\n`);
|
|
495
|
+
devtunnelInstalled = false;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
453
498
|
}
|
|
454
499
|
}
|
|
455
500
|
if (devtunnelInstalled) {
|
|
@@ -556,6 +601,27 @@ async function main() {
|
|
|
556
601
|
cols, rows, cwd,
|
|
557
602
|
env: safeEnv,
|
|
558
603
|
});
|
|
604
|
+
// Detect CSPRNG crash (Node.js + node-pty + ConPTY issue) and retry with useConpty: false
|
|
605
|
+
let ptyExitedEarly = false;
|
|
606
|
+
const earlyExitCheck = new Promise((resolve) => {
|
|
607
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
608
|
+
if (exitCode === 134 || exitCode === 3221226505) { // 134 = SIGABRT, 3221226505 = STATUS_BREAKPOINT
|
|
609
|
+
ptyExitedEarly = true;
|
|
610
|
+
resolve();
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
setTimeout(resolve, 2000); // Wait 2s — if still running, it's fine
|
|
614
|
+
});
|
|
615
|
+
await earlyExitCheck;
|
|
616
|
+
if (ptyExitedEarly && process.platform === 'win32') {
|
|
617
|
+
console.log(` ${YELLOW}⚠${RESET} ConPTY crash detected, retrying with legacy PTY backend...\n`);
|
|
618
|
+
ptyProcess = nodePty.spawn(resolvedCmd, commandArgs, {
|
|
619
|
+
name: 'xterm-256color',
|
|
620
|
+
cols, rows, cwd,
|
|
621
|
+
env: safeEnv,
|
|
622
|
+
useConpty: false,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
559
625
|
ptyProcess.onData((data) => {
|
|
560
626
|
process.stdout.write(data);
|
|
561
627
|
broadcast(data);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cli-tunnel",
|
|
3
|
-
"version": "1.2.0-beta.
|
|
4
|
-
"description": "Tunnel any CLI app to your phone
|
|
3
|
+
"version": "1.2.0-beta.2",
|
|
4
|
+
"description": "Tunnel any CLI app to your phone — PTY + devtunnel + xterm.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|