Ciro Iriarte created GUACAMOLE-2307:
---------------------------------------
Summary: Add opt-in raw text-output mode for terminal protocols
(SSH/telnet/Kubernetes)
Key: GUACAMOLE-2307
URL: https://issues.apache.org/jira/browse/GUACAMOLE-2307
Project: Guacamole
Issue Type: New Feature
Components: guacamole-server
Reporter: Ciro Iriarte
h2. Summary
Add an opt-in per-connection parameter, {{text-output}}, that makes guacd tee
the
raw remote PTY byte stream (including ANSI/CSI/OSC sequences) to an outbound
Guacamole {{pipe}} stream named {{STDOUT}}, for the SSH, telnet, and Kubernetes
protocols. This lets a non-graphical / native CLI client present a terminal
session
as true in-terminal text instead of decoding rasterized glyphs. Default off; no
change to existing connections.
h2. Motivation
The browser terminal renders output graphically: guacd runs the emulator and
emits
only drawing instructions, so no character codepoints reach the client. A
native or
CLI client therefore cannot reconstruct the text of a session. This adds a
faithful
raw byte channel in the output direction, complementing the inbound STDIN pipe
(GUACAMOLE-574). It deliberately does *not* reuse the {{guacctl}} pipe redirect
(GUACAMOLE-17): that path runs through {{guac_terminal_echo()}}, which strips
ESC/CSI/OSC sequences and so cannot carry a faithful raw stream.
h2. Design
* New connection parameter {{text-output}} (string enum):
** {{true}} — tee mode: raw bytes are teed to STDOUT *and* the graphical
display is
still rendered, so browser users are unaffected.
** {{raw}} — headless mode: the graphical terminal is not rendered at all;
bytes are
delivered only via STDOUT, eliminating the graphical instruction stream and
its
render cost.
** anything else, including {{false}} or omitted — disabled.
* guacd opens an outbound {{pipe}} stream named {{STDOUT}}, mimetype
{{application/octet-stream}} (advisory; payload is raw bytes carried in base64
{{blob}} instructions, so it is binary-safe and charset-agnostic — the client
decodes the bytes itself).
* The tee is taken *at the protocol source* — in each protocol's PTY read path,
upstream of the terminal emulator — via a dedicated {{text_output_stream}} on
{{guac_terminal}}, independent of the {{guacctl}} pipe machinery.
* The stream is allocated on the connection *owner's* user socket
({{guac_user_alloc_stream}}), not broadcast to every user, and uses an even
(ack-routable) stream index so client {{ack}} instructions return to the
stream.
* New additive public API in {{terminal.h}}:
{{guac_terminal_text_output_open/write/flush/close}} (+ a {{should_open}}
helper).
No existing symbol is changed.
* In {{raw}} mode the read loop skips {{guac_terminal_write()}} (the terminal
is kept
for input/size/stdin) and flushes per-read, since there is no graphical frame
cycle.
h2. Flow control / backpressure
This is the subtle part and is worth reviewer attention:
* Clients must {{ack}} every {{blob}}, on receipt rather than after rendering.
* guacd bounds the unacknowledged backlog by *bytes*: 256 KB
({{GUAC_TERMINAL_TEXT_OUTPUT_MAX_INFLIGHT_BYTES}}), plus a 256-blob cap
purely to
bound a small size-tracking FIFO. The byte bound is the operative one, because
{{raw}} mode emits one blob per PTY read and those blobs are often only a few
bytes.
* On a stalled consumer the response differs by mode, deliberately:
** {{tee}}: buffered output is *dropped* and the session continues. The tee
shares the
protocol read loop with the graphical display, so blocking on a stalled text
consumer would also stall any co-attached browser user.
** {{raw}}: the read loop is *throttled* — the writer waits (on a condition
variable)
for the window to drain, which propagates backpressure to the remote program
through the PTY exactly as a slow local terminal would. Raw renders nothing
graphically, so pausing starves no one and the byte stream stays intact.
* A consumer that stops acking entirely for 15 s
({{GUAC_TERMINAL_TEXT_OUTPUT_STALL_TIMEOUT}}) is disconnected with
{{SERVER_ERROR}}.
The wait also short-circuits immediately once the connection is going away,
so an
abrupt disconnect does not delay teardown.
h2. Security
* {{text-output}} is effectively a copy/export channel, so it is gated behind
{{disable-copy}}: guacd refuses to open the pipe when copying from the
terminal is
disabled (and logs a warning).
* Pipe contents are not logged or recorded by default (output may echo secrets).
* Intended trust model: the parameter is admin-provisioned on the connection
(not
client-requestable); guacd is reached only over the authenticated
tunnel/gateway.
No new port, no new protocol instruction (reuses
{{pipe}}/{{blob}}/{{end}}/{{ack}}),
no authentication change.
h2. Backwards compatibility / ABI
Additive and opt-in, default off. No new protocol instruction, no DB schema
change,
and no web-app change is required — an unmodified guacamole-client already
passes the
parameter through, since {{ConfiguredGuacamoleSocket}} builds the {{connect}}
instruction from the argument list guacd announces in its handshake. No ABI
break:
only additive public symbols are introduced.
h2. Validation
* CUnit unit tests for terminal text-output: disable-copy gating, owner-stream
+ ack
routing, tee drop-on-stall, raw throttle-vs-abort, the byte bound, ACK
retiring the
oldest blob's byte count, surplus-ACK safety, disconnect short-circuit, and
closed-stream-unroutable. Full {{make check}} is green on Debian 12 and
openSUSE
Leap 16.
* An end-to-end harness (direct-to-guacd, no web app) covers
SSH/telnet/Kubernetes
across 13 scenarios: positive tee, negative (default-off and disable-copy ->
no
pipe), a byte-bound flood with and without acks, small-unacked-not-dropped,
and
raw (text delivered, graphics suppressed). All pass.
* Validated live through an unmodified guacamole-client gateway with a native
CLI
client, in both tee and raw modes, including a 20,000-line flood and the 15 s
give-up path (measured: 256 KB delivered, then a clean SERVER_ERROR abort).
h2. Patch / branch
Implemented on a fork branch, 23 commits atop apache/main (base 6719b20d). A
redistributable {{git format-patch}} artifact is available and applies cleanly
onto
that base. Happy to open a PR against apache/main. The client side is UI-only
convenience (protocol JSON enum + i18n strings), and a matching manual section
documents the parameter and the backpressure contract.
h2. Related
* GUACAMOLE-17 (resolved) — the original {{guacctl}} output-to-pipe redirect;
routes
through the escape-stripping echo path, so it is not usable for a faithful raw
stream. This issue is the complement, taken at the source.
* GUACAMOLE-574 — inbound STDIN pipe (the input direction).
--
This message was sent by Atlassian Jira
(v8.20.10#820010)