You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@guacamole.apache.org by "Ciro Iriarte (Jira)" <ji...@apache.org> on 2026/07/26 01:02:00 UTC

[jira] [Created] (GUACAMOLE-2307) Add opt-in raw text-output mode for terminal protocols (SSH/telnet/Kubernetes)

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)