From 1d0819ddb614c2c7030f70f46683e1a84a462d37 Mon Sep 17 00:00:00 2001 From: barry Date: Thu, 25 Jun 2026 23:12:28 +0800 Subject: [PATCH 1/2] feat: add tty-fwd applet to bridge PTY to cloud over SSH -B Run a shell in a local PTY and attach it to dbclient netcat mode via stdin/stdout, so devices can expose a terminal to a remote TCP endpoint without listening on any local port. Co-authored-by: Cursor --- Makefile.in | 4 +- README.md | 22 ++++++ build.zig | 2 + src/dbmulti.c | 13 ++++ src/tty-fwd.c | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 src/tty-fwd.c diff --git a/Makefile.in b/Makefile.in index a26f0112..cf77b1fe 100644 --- a/Makefile.in +++ b/Makefile.in @@ -238,8 +238,8 @@ scp: $(SCPOBJS) $(HEADERS) Makefile # multi-binary compilation. MULTIOBJS= ifeq ($(MULTI),1) - MULTIOBJS=$(OBJ_DIR)/dbmulti.o $(sort $(foreach prog, $(PROGRAMS), $($(prog)objs))) - CPPFLAGS+=$(addprefix -DDBMULTI_, $(PROGRAMS)) -DDROPBEAR_MULTI + MULTIOBJS=$(OBJ_DIR)/dbmulti.o $(OBJ_DIR)/tty-fwd.o $(sort $(foreach prog, $(PROGRAMS), $($(prog)objs))) + CPPFLAGS+=$(addprefix -DDBMULTI_, $(PROGRAMS)) -DDBMULTI_ttyfwd -DDROPBEAR_MULTI endif dropbearmulti$(EXEEXT): $(HEADERS) $(MULTIOBJS) $(LIBTOM_DEPS) Makefile diff --git a/README.md b/README.md index 2457539d..0edb30cc 100644 --- a/README.md +++ b/README.md @@ -144,3 +144,25 @@ Recommended flow: generate a high-entropy one-time password, start a temporary dropbear with `DROPBEAR_OTP` set (typically reached over a tunnel), use it, then stop that server. Keep `DROPBEAR_SVR_OTP_PASSWORD` at `0` in builds that don't need it. + +### Cloud terminal bridge — `tty-fwd` + +For appliances that expose an internal shell to a cloud console over SSH without +listening on a local port. `tty-fwd` runs a shell in a PTY and connects it to +`dbclient` netcat mode (`-B`): bytes flow over an outbound SSH connection to a +TCP endpoint on the remote side (where your web UI attaches). + +```sh +tty-fwd -y -i /factory/tunnel_key -B 127.0.0.1:9000 tunnel@jump.example.com +``` + +| Option | Meaning | +|--------|---------| +| `--shell path` | Shell to run (default: `DROPBEAR_FORCE_SHELL` or `/bin/sh`) | +| `-B host:port` | Remote TCP endpoint (on the SSH server / cloud side) | +| other flags | Passed through to `dbclient` (`-i`, `-y`, `-K`, etc.) | + +The device makes no inbound connection and opens no local listen socket. +On the cloud host, something must accept connections on the forwarded port +(for example `127.0.0.1:9000` when using `-B 127.0.0.1:9000`) and bridge them +to your browser terminal. diff --git a/build.zig b/build.zig index 1dfe1acd..d0e39756 100644 --- a/build.zig +++ b/build.zig @@ -116,6 +116,7 @@ pub fn build(b: *std.Build) void { "-DDBMULTI_dropbearkey", "-DDBMULTI_dropbearconvert", "-DDBMULTI_scp", + "-DDBMULTI_ttyfwd", "-DPROGRESS_METER", }) catch @panic("OOM"); @@ -161,6 +162,7 @@ pub fn build(b: *std.Build) void { const dropbear_sources = [_][]const u8{ // dbmulti dispatcher "dbmulti.c", + "tty-fwd.c", // COMMONOBJS "dbutil.c", "buffer.c", diff --git a/src/dbmulti.c b/src/dbmulti.c index 919ce3d3..22d7a84b 100644 --- a/src/dbmulti.c +++ b/src/dbmulti.c @@ -25,6 +25,10 @@ #include "includes.h" #include "dbutil.h" +#if defined(DBMULTI_ttyfwd) && DROPBEAR_MULTI +int tty_fwd_main(int argc, char ** argv); +#endif + static int runprog(const char *multipath, const char *progname, int argc, char ** argv, int *match) { *match = DROPBEAR_SUCCESS; @@ -55,6 +59,12 @@ static int runprog(const char *multipath, if (strcmp(progname, "scp") == 0) { return scp_main(argc, argv); } +#endif +#ifdef DBMULTI_ttyfwd + if (strcmp(progname, "tty-fwd") == 0 + || strcmp(progname, "ttyfwd") == 0) { + return tty_fwd_main(argc, argv); + } #endif *match = DROPBEAR_FAILURE; return 1; @@ -96,6 +106,9 @@ int main(int argc, char ** argv) { #endif #ifdef DBMULTI_scp "'scp' - secure copy\n" +#endif +#ifdef DBMULTI_ttyfwd + "'tty-fwd' - forward a local PTY over SSH (netcat mode)\n" #endif , DROPBEAR_VERSION); diff --git a/src/tty-fwd.c b/src/tty-fwd.c new file mode 100644 index 00000000..86fc56b5 --- /dev/null +++ b/src/tty-fwd.c @@ -0,0 +1,202 @@ +/* + * tty-fwd - forward a local PTY to a remote TCP endpoint over SSH. + * + * Runs the given shell in a pseudo-terminal and connects the PTY master to + * dbclient netcat mode (-B) via stdin/stdout. No local listen port is opened; + * the device only makes an outbound SSH connection. + * + * Example: + * tty-fwd -y -i /factory/key -B 127.0.0.1:9000 tunnel@jump.example.com + */ + +#include "includes.h" +#include "dbutil.h" +#include "sshpty.h" + +#if !DROPBEAR_CLI_NETCAT +#error "tty-fwd requires DROPBEAR_CLI_NETCAT" +#endif + +#if DROPBEAR_MULTI && defined(DBMULTI_dbclient) +int cli_main(int argc, char **argv); +#endif + +#if defined(DROPBEAR_FORCE_SHELL) +#define TTYFWD_DEFAULT_SHELL DROPBEAR_FORCE_SHELL +#else +#define TTYFWD_DEFAULT_SHELL "/bin/sh" +#endif + +static pid_t shell_pid = -1; + +static void shell_cleanup(void) { + if (shell_pid <= 0) { + return; + } + kill(shell_pid, SIGHUP); + (void)waitpid(shell_pid, NULL, 0); + shell_pid = -1; +} + +static void shell_signal_handler(int sig) { + (void)sig; + shell_cleanup(); + _exit(EXIT_FAILURE); +} + +static void printhelp(const char *prog) { + fprintf(stderr, + "Usage: %s [--shell path] [dbclient options] -B host:port [user@]remotehost\n" + "\n" + "Run a shell in a local PTY and forward its I/O to a remote TCP endpoint\n" + "over SSH (dbclient netcat mode). The device does not listen on any port.\n" + "\n" + " --shell path Shell to run (default: %s)\n" + " -h, --help Show this help\n" + "\n" + "All other options are passed to dbclient. -B host:port is required.\n" + "\n" + "Example:\n" + " %s -y -i /factory/key -B 127.0.0.1:9000 tunnel@jump.example.com\n", + prog, TTYFWD_DEFAULT_SHELL, prog); +} + +/* True if argv contains dbclient's -B netcat option. */ +static int argv_has_netcat_opt(int argc, char **argv) { + int i, j; + + for (i = 1; i < argc; i++) { + if (argv[i][0] != '-') { + continue; + } + for (j = 1; argv[i][j]; j++) { + if (argv[i][j] != 'B') { + continue; + } + if (argv[i][j + 1] != '\0') { + return 1; + } + if (i + 1 < argc) { + return 1; + } + return 0; + } + } + return 0; +} + +/* Parse --shell / -h / --, compact argv for cli_main with argv[0]="dbclient". */ +static void parse_ttyfwd_opts(int *argc, char ***argv, const char **shell) { + char **args = *argv; + int count = *argc; + int i = 1; + int dest = 1; + + *shell = TTYFWD_DEFAULT_SHELL; + + while (i < count) { + if (strcmp(args[i], "--shell") == 0) { + if (i + 1 >= count) { + dropbear_exit("--shell requires an argument"); + } + *shell = args[++i]; + i++; + continue; + } + if (strcmp(args[i], "-h") == 0 || strcmp(args[i], "--help") == 0) { + printhelp(args[0]); + exit(EXIT_SUCCESS); + } + if (strcmp(args[i], "--") == 0) { + i++; + break; + } + if (args[i][0] == '-') { + /* dbclient option */ + break; + } + /* remote host or other positional arg for dbclient */ + break; + } + + args[0] = "dbclient"; + memmove(&args[1], &args[i], (size_t)(count - i) * sizeof(char *)); + dest = count - i + 1; + args[dest] = NULL; + *argc = dest; + *argv = args; +} + +static void start_shell(const char *shell, int master, int slave, const char *tty_name) { + struct passwd *pw; + + shell_pid = fork(); + if (shell_pid < 0) { + dropbear_exit("fork failed: %s", strerror(errno)); + } + + if (shell_pid != 0) { + return; + } + + /* child */ + close(master); + pty_make_controlling_tty(&slave, tty_name); + + if (dup2(slave, STDIN_FILENO) < 0 + || dup2(slave, STDOUT_FILENO) < 0 + || dup2(slave, STDERR_FILENO) < 0) { + _exit(EXIT_FAILURE); + } + close(slave); + + pw = getpwuid(getuid()); + if (pw) { + pty_setowner(pw, (char*)tty_name); + } + + execl(shell, shell, (char*)NULL); + _exit(EXIT_FAILURE); +} + +#if defined(DBMULTI_ttyfwd) && DROPBEAR_MULTI +int tty_fwd_main(int argc, char **argv) { +#else +int main(int argc, char **argv) { +#endif + const char *shell; + int master = -1; + int slave = -1; + char tty_name[64]; + + parse_ttyfwd_opts(&argc, &argv, &shell); + + if (!argv_has_netcat_opt(argc, argv)) { + dropbear_exit("tty-fwd requires dbclient -B host:port (netcat mode)"); + } + + if (pty_allocate(&master, &slave, tty_name, sizeof(tty_name)) == 0) { + dropbear_exit("Failed to allocate pty"); + } + + start_shell(shell, master, slave, tty_name); + close(slave); + + if (signal(SIGINT, shell_signal_handler) == SIG_ERR + || signal(SIGTERM, shell_signal_handler) == SIG_ERR) { + dropbear_exit("signal() error"); + } + atexit(shell_cleanup); + + if (dup2(master, STDIN_FILENO) < 0 || dup2(master, STDOUT_FILENO) < 0) { + dropbear_exit("dup2 failed: %s", strerror(errno)); + } + close(master); + +#if DROPBEAR_MULTI && defined(DBMULTI_dbclient) + return cli_main(argc, argv); +#else + dropbear_exit("tty-fwd requires a multi-call binary with dbclient"); + return EXIT_FAILURE; +#endif +} From ca4fb5a146a213b19c5e8050b325a7a9a86d4329 Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 26 Jun 2026 00:31:59 +0800 Subject: [PATCH 2/2] feat: set PTY TERM/size in tty-fwd and add cloud-terminal demo Give forwarded shells a fixed winsize and TERM so full-screen programs work over the bridge, and add a small WebSocket/xterm.js demo to test tty-fwd end-to-end over SSH -B. Co-authored-by: Cursor --- examples/cloud-terminal/.gitignore | 1 + examples/cloud-terminal/README.md | 60 ++++++++ examples/cloud-terminal/package-lock.json | 36 +++++ examples/cloud-terminal/package.json | 14 ++ examples/cloud-terminal/public/index.html | 67 +++++++++ examples/cloud-terminal/server.js | 162 ++++++++++++++++++++++ src/tty-fwd.c | 60 +++++++- 7 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 examples/cloud-terminal/.gitignore create mode 100644 examples/cloud-terminal/README.md create mode 100644 examples/cloud-terminal/package-lock.json create mode 100644 examples/cloud-terminal/package.json create mode 100644 examples/cloud-terminal/public/index.html create mode 100644 examples/cloud-terminal/server.js diff --git a/examples/cloud-terminal/.gitignore b/examples/cloud-terminal/.gitignore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/examples/cloud-terminal/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/examples/cloud-terminal/README.md b/examples/cloud-terminal/README.md new file mode 100644 index 00000000..f49d6ae7 --- /dev/null +++ b/examples/cloud-terminal/README.md @@ -0,0 +1,60 @@ +# Cloud Terminal Demo (for `tty-fwd`) + +Bridges a device terminal stream (delivered over SSH by `tty-fwd`) to a browser +terminal (xterm.js over WebSocket). The web UI lives here on the cloud side; the +device only makes an outbound SSH connection and opens **no local port**. + +``` +browser ──WS──► this demo (HTTP+WS) ──┐ + │ pairs +device: /bin/sh ─PTY─ tty-fwd ─SSH─► dropbear ─► 127.0.0.1:9000 (this demo TCP) +``` + +## Run + +```sh +cd examples/cloud-terminal +npm install +node server.js # UI on :8080, device stream on 127.0.0.1:9000 +``` + +Options: `--http ` (browser UI), `--tcp ` (device stream target, +must match the device's `-B`), `--host ` (TCP bind, default 127.0.0.1). + +Open . + +## Quick local test (no SSH needed) + +Simulate the device side with `socat`, connecting a local PTY shell straight to +the demo's TCP port: + +```sh +socat exec:'/bin/sh -i',pty,setsid,ctty,stderr tcp:127.0.0.1:9000 +``` + +Then open the browser UI — you should get an interactive shell. This verifies +the WebSocket/xterm path end to end. + +## Full test with the real tunnel + +On the cloud host run a dropbear server and this demo. On the device: + +```sh +dropbearmulti tty-fwd --shell /bin/sh \ + -y -i /factory/tunnel_key \ + -B 127.0.0.1:9000 \ + tunnel@cloud-host +``` + +`-B 127.0.0.1:9000` tells the cloud dropbear to connect to this demo's TCP +listener; the PTY stream is bridged to whichever browser is connected. + +## Notes / limitations + +- Raw byte stream only: terminal **resize is not propagated** to the device PTY + (xterm fits the browser window, but the remote PTY keeps the size `tty-fwd` + allocated). Adding a small control channel is a possible future enhancement. +- FIFO pairing: one device socket is paired with one browser. For multiple + devices you would add session IDs / routing. +- For production, terminate TLS (wss://) and authenticate the browser side at + the cloud; restrict the TCP listener to localhost and only reachable via SSH. diff --git a/examples/cloud-terminal/package-lock.json b/examples/cloud-terminal/package-lock.json new file mode 100644 index 00000000..a5562803 --- /dev/null +++ b/examples/cloud-terminal/package-lock.json @@ -0,0 +1,36 @@ +{ + "name": "dropbear-cloud-terminal", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dropbear-cloud-terminal", + "version": "1.0.0", + "dependencies": { + "ws": "^8.18.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/examples/cloud-terminal/package.json b/examples/cloud-terminal/package.json new file mode 100644 index 00000000..dd7e5234 --- /dev/null +++ b/examples/cloud-terminal/package.json @@ -0,0 +1,14 @@ +{ + "name": "dropbear-cloud-terminal", + "version": "1.0.0", + "private": true, + "description": "Cloud-side bridge: SSH -B terminal stream <-> WebSocket xterm.js", + "type": "commonjs", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "ws": "^8.18.0" + } +} diff --git a/examples/cloud-terminal/public/index.html b/examples/cloud-terminal/public/index.html new file mode 100644 index 00000000..ca635c5b --- /dev/null +++ b/examples/cloud-terminal/public/index.html @@ -0,0 +1,67 @@ + + + + + +Dropbear Cloud Terminal + + + + +
+ Dropbear Cloud Terminal + connecting… +
+
+ + + + + + diff --git a/examples/cloud-terminal/server.js b/examples/cloud-terminal/server.js new file mode 100644 index 00000000..dbc2c761 --- /dev/null +++ b/examples/cloud-terminal/server.js @@ -0,0 +1,162 @@ +'use strict'; + +/* + * Cloud-side terminal bridge for dropbear `tty-fwd`. + * + * Two sides are connected here: + * + * 1. Device terminal stream (raw bytes): + * The device runs `tty-fwd ... -B 127.0.0.1: tunnel@thishost`. + * dbclient netcat mode makes the SSH server open a TCP connection to + * 127.0.0.1: on this host, carrying the PTY stdin/stdout. + * So this process LISTENS on and each accepted socket is one + * device terminal. + * + * 2. Browser (xterm.js over WebSocket): + * The browser loads index.html and opens ws://thishost:/ws. + * + * A device socket and a browser WebSocket are paired FIFO and piped both ways. + * Raw bytes only - there is no resize/control channel back to the device PTY + * (a known limitation; the PTY size is whatever tty-fwd allocated). + */ + +const http = require('http'); +const net = require('net'); +const fs = require('fs'); +const path = require('path'); +const { WebSocketServer } = require('ws'); + +function parseArgs(argv) { + const opts = { http: 8080, tcp: 9000, host: '127.0.0.1' }; + for (let i = 2; i < argv.length; i++) { + const a = argv[i]; + if (a === '--http') opts.http = parseInt(argv[++i], 10); + else if (a === '--tcp') opts.tcp = parseInt(argv[++i], 10); + else if (a === '--host') opts.host = argv[++i]; + else if (a === '-h' || a === '--help') { + console.log( + 'Usage: node server.js [--http 8080] [--tcp 9000] [--host 127.0.0.1]\n' + + ' --http port for the browser UI + WebSocket\n' + + ' --tcp port the device terminal stream connects to (dropbear -B target)\n' + + ' --host bind address for the TCP listener (default 127.0.0.1)' + ); + process.exit(0); + } + } + return opts; +} + +const opts = parseArgs(process.argv); + +/* FIFO pairing queues. */ +const waitingDevices = []; // net.Socket +const waitingClients = []; // ws + +function pair(device, ws) { + let active = true; + + const onDeviceData = (buf) => { + if (ws.readyState === ws.OPEN) ws.send(buf); + }; + const onDeviceEnd = () => { + console.log('[pair] device closed'); + teardown(true); + }; + const onWsMessage = (data) => { + const buf = Buffer.isBuffer(data) ? data : Buffer.from(data); + if (!device.destroyed) device.write(buf); + }; + const onWsEnd = () => { + console.log('[pair] browser closed'); + /* Keep the device TCP stream alive so tty-fwd survives page refresh. */ + teardown(false); + }; + + function teardown(deviceGone) { + if (!active) return; + active = false; + device.removeListener('data', onDeviceData); + device.removeListener('close', onDeviceEnd); + device.removeListener('error', onDeviceEnd); + ws.removeListener('message', onWsMessage); + ws.removeListener('close', onWsEnd); + ws.removeListener('error', onWsEnd); + if (deviceGone) { + try { ws.close(); } catch (_) {} + } else if (!device.destroyed) { + console.log('[pair] device back in queue (browser refresh OK)'); + enqueueDevice(device); + } + } + + console.log('[pair] device <-> browser connected'); + device.on('data', onDeviceData); + device.on('close', onDeviceEnd); + device.on('error', onDeviceEnd); + ws.on('message', onWsMessage); + ws.on('close', onWsEnd); + ws.on('error', onWsEnd); +} + +function enqueueDevice(socket) { + const ws = waitingClients.shift(); + if (ws) pair(socket, ws); + else { + waitingDevices.push(socket); + socket.on('close', () => { + const idx = waitingDevices.indexOf(socket); + if (idx >= 0) waitingDevices.splice(idx, 1); + }); + } +} + +function enqueueClient(ws) { + const device = waitingDevices.shift(); + if (device) pair(device, ws); + else { + waitingClients.push(ws); + ws.on('close', () => { + const idx = waitingClients.indexOf(ws); + if (idx >= 0) waitingClients.splice(idx, 1); + }); + if (ws.readyState === ws.OPEN) { + ws.send(Buffer.from('\r\n[cloud-terminal] waiting for a device to connect...\r\n')); + } + } +} + +/* ---- Device terminal listener (raw TCP) -------------------------------- */ +const tcpServer = net.createServer((socket) => { + socket.setNoDelay(true); + console.log(`[tcp] device connected from ${socket.remoteAddress}:${socket.remotePort}`); + enqueueDevice(socket); +}); +tcpServer.listen(opts.tcp, opts.host, () => { + console.log(`[tcp] listening on ${opts.host}:${opts.tcp} (dropbear -B target)`); +}); + +/* ---- Browser UI + WebSocket -------------------------------------------- */ +const indexPath = path.join(__dirname, 'public', 'index.html'); + +const httpServer = http.createServer((req, res) => { + if (req.url === '/' || req.url === '/index.html') { + fs.readFile(indexPath, (err, body) => { + if (err) { res.writeHead(500); res.end('index.html missing'); return; } + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(body); + }); + return; + } + res.writeHead(404); + res.end('not found'); +}); + +const wss = new WebSocketServer({ server: httpServer, path: '/ws' }); +wss.on('connection', (ws) => { + console.log('[ws] browser connected'); + enqueueClient(ws); +}); + +httpServer.listen(opts.http, () => { + console.log(`[http] terminal UI on http://localhost:${opts.http}/`); +}); diff --git a/src/tty-fwd.c b/src/tty-fwd.c index 86fc56b5..cb8d0239 100644 --- a/src/tty-fwd.c +++ b/src/tty-fwd.c @@ -27,8 +27,19 @@ int cli_main(int argc, char **argv); #define TTYFWD_DEFAULT_SHELL "/bin/sh" #endif +#define TTYFWD_DEFAULT_TERM "xterm-256color" +#define TTYFWD_DEFAULT_ROWS 24 +#define TTYFWD_DEFAULT_COLS 80 + static pid_t shell_pid = -1; +/* Initial PTY geometry / terminal type for the forwarded shell. There is no + * resize channel back from the cloud side, so these define the fixed terminal + * size full-screen programs (top, vi) will use. */ +static const char *ttyfwd_term = TTYFWD_DEFAULT_TERM; +static int ttyfwd_rows = TTYFWD_DEFAULT_ROWS; +static int ttyfwd_cols = TTYFWD_DEFAULT_COLS; + static void shell_cleanup(void) { if (shell_pid <= 0) { return; @@ -52,13 +63,17 @@ static void printhelp(const char *prog) { "over SSH (dbclient netcat mode). The device does not listen on any port.\n" "\n" " --shell path Shell to run (default: %s)\n" + " --term name TERM for the shell (default: %s)\n" + " --rows n Initial PTY rows (default: %d)\n" + " --cols n Initial PTY cols (default: %d)\n" " -h, --help Show this help\n" "\n" "All other options are passed to dbclient. -B host:port is required.\n" "\n" "Example:\n" " %s -y -i /factory/key -B 127.0.0.1:9000 tunnel@jump.example.com\n", - prog, TTYFWD_DEFAULT_SHELL, prog); + prog, TTYFWD_DEFAULT_SHELL, TTYFWD_DEFAULT_TERM, + TTYFWD_DEFAULT_ROWS, TTYFWD_DEFAULT_COLS, prog); } /* True if argv contains dbclient's -B netcat option. */ @@ -103,6 +118,36 @@ static void parse_ttyfwd_opts(int *argc, char ***argv, const char **shell) { i++; continue; } + if (strcmp(args[i], "--term") == 0) { + if (i + 1 >= count) { + dropbear_exit("--term requires an argument"); + } + ttyfwd_term = args[++i]; + i++; + continue; + } + if (strcmp(args[i], "--rows") == 0) { + if (i + 1 >= count) { + dropbear_exit("--rows requires an argument"); + } + ttyfwd_rows = atoi(args[++i]); + if (ttyfwd_rows <= 0) { + dropbear_exit("--rows must be positive"); + } + i++; + continue; + } + if (strcmp(args[i], "--cols") == 0) { + if (i + 1 >= count) { + dropbear_exit("--cols requires an argument"); + } + ttyfwd_cols = atoi(args[++i]); + if (ttyfwd_cols <= 0) { + dropbear_exit("--cols must be positive"); + } + i++; + continue; + } if (strcmp(args[i], "-h") == 0 || strcmp(args[i], "--help") == 0) { printhelp(args[0]); exit(EXIT_SUCCESS); @@ -143,6 +188,19 @@ static void start_shell(const char *shell, int master, int slave, const char *tt close(master); pty_make_controlling_tty(&slave, tty_name); + /* Give the PTY a fixed initial size so full-screen programs (top, vi) + * can position the cursor and redraw. There is no resize channel back + * from the cloud, so this size stays constant for the session. */ + { + struct winsize ws; + memset(&ws, 0, sizeof(ws)); + ws.ws_row = (unsigned short)ttyfwd_rows; + ws.ws_col = (unsigned short)ttyfwd_cols; + (void)ioctl(slave, TIOCSWINSZ, &ws); + } + + setenv("TERM", ttyfwd_term, 1); + if (dup2(slave, STDIN_FILENO) < 0 || dup2(slave, STDOUT_FILENO) < 0 || dup2(slave, STDERR_FILENO) < 0) {