Commit 5d8434fc for guacamole.apache.org

commit 5d8434fc1d70902267caf31585dc97325c42db7d
Author: Ciro Iriarte <ciro.iriarte@gmail.com>
Date:   Fri Jul 17 12:25:34 2026 -0300

    GUACAMOLE-2301: Add 'telnet' network-protocol transport for serial

    Add network-protocol=telnet: base RFC854 Telnet framing (IAC escaping,
    BINARY/SGA/ECHO negotiation) via libtelnet, without the RFC2217 COM-PORT
    option. Serial line settings are informational, as with raw. Intended for
    endpoints that speak plain Telnet rather than RFC2217 -- QEMU/KVM telnet:
    serial ports, VMware ESXi telnet:// virtual serial ports, and telnet-mode
    console servers.

    Reuses the existing libtelnet event handler and send/recv/free, and
    acquires its socket through guac_serial_net_open_fd(), so it works in both
    forward and reverse mode. A Send Break is delivered as the Telnet BRK
    command. Gated on libtelnet (ENABLE_SERIAL_RFC2217) with no-op stubs when
    libtelnet is absent.

diff --git a/src/protocols/serial/rfc2217.c b/src/protocols/serial/rfc2217.c
index c7dfc333..4a061a62 100644
--- a/src/protocols/serial/rfc2217.c
+++ b/src/protocols/serial/rfc2217.c
@@ -51,6 +51,14 @@
 #define TELNET_TELOPT_COMPORT 44
 #endif

+/**
+ * The Telnet BREAK (BRK) command, sent as "IAC BRK". libtelnet does not define
+ * this command, so it is defined here.
+ */
+#ifndef TELNET_BREAK
+#define TELNET_BREAK 243   /* Telnet BRK command (RFC 854) */
+#endif
+
 /**
  * COM-PORT-OPTION client-to-server command: set the serial line speed. The
  * data is a 4-byte big-endian unsigned integer giving the speed in bits per
@@ -277,6 +285,66 @@ int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,

 }

+int guac_serial_telnet_open(guac_serial_stream* stream, guac_client* client,
+        guac_serial_settings* settings) {
+
+    /* Support levels for the telnet options used by the base Telnet session.
+     * Unlike RFC2217, no COM-PORT-OPTION is negotiated. */
+    static const telnet_telopt_t telnet_options[] = {
+        { TELNET_TELOPT_BINARY, TELNET_WILL, TELNET_DO   },
+        { TELNET_TELOPT_SGA,    TELNET_WILL, TELNET_DO   },
+        { TELNET_TELOPT_ECHO,   TELNET_WONT, TELNET_DO   },
+        { -1, 0, 0 }
+    };
+
+    /* Acquire the network socket (dialing out, or accepting an inbound
+     * connection in reverse mode). On failure open_status is already set. */
+    int fd = guac_serial_net_open_fd(stream, client, settings);
+    if (fd < 0)
+        return -1;
+
+    /* Store the socket before initializing libtelnet, as the event handler
+     * writes outbound data to this descriptor */
+    stream->backend = GUAC_SERIAL_BACKEND_TELNET;
+    stream->fd = fd;
+
+    /* Initialize the libtelnet session, reusing the shared event handler and
+     * passing the stream as user data */
+    telnet_t* telnet = telnet_init(telnet_options,
+            guac_serial_rfc2217_event_handler, 0, stream);
+    if (telnet == NULL) {
+        guac_client_log(client, GUAC_LOG_ERROR,
+                "Telnet client allocation failed.");
+        stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
+        close(fd);
+        stream->fd = -1;
+        return -1;
+    }
+
+    stream->telnet = telnet;
+
+    /* Advertise our willingness to use binary mode and suppress go-ahead */
+    telnet_negotiate(telnet, TELNET_WILL, TELNET_TELOPT_BINARY);
+    telnet_negotiate(telnet, TELNET_DO, TELNET_TELOPT_BINARY);
+    telnet_negotiate(telnet, TELNET_WILL, TELNET_TELOPT_SGA);
+
+    /* Plain telnet cannot negotiate serial line parameters; they remain
+     * informational, so no COM-PORT-OPTION line push is performed. */
+
+    return 0;
+
+}
+
+int guac_serial_telnet_send_break(guac_serial_stream* stream) {
+
+    /* Plain telnet has no COM-PORT-OPTION break, so the NVT BREAK function is
+     * used instead. Its effect depends entirely on the remote end. telnet_iac
+     * sends "IAC <cmd>", i.e. "IAC BRK". */
+    telnet_iac((telnet_t*) stream->telnet, TELNET_BREAK);
+    return 0;
+
+}
+
 void guac_serial_rfc2217_send(guac_serial_stream* stream, const char* buffer,
         int length) {
     telnet_send((telnet_t*) stream->telnet, buffer, length);
@@ -331,6 +399,24 @@ int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,

 }

+int guac_serial_telnet_open(guac_serial_stream* stream, guac_client* client,
+        guac_serial_settings* settings) {
+
+    (void) settings;
+
+    guac_client_log(client, GUAC_LOG_ERROR, "Telnet transport support was not "
+            "compiled in (rebuild guacamole-server with libtelnet).");
+    stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
+    return -1;
+
+}
+
+int guac_serial_telnet_send_break(guac_serial_stream* stream) {
+    /* Telnet unavailable; a stream with this backend is never created */
+    (void) stream;
+    return 0;
+}
+
 void guac_serial_rfc2217_send(guac_serial_stream* stream, const char* buffer,
         int length) {
     /* RFC2217 unavailable; a stream with this backend is never created */
diff --git a/src/protocols/serial/rfc2217.h b/src/protocols/serial/rfc2217.h
index adb44546..12845ea6 100644
--- a/src/protocols/serial/rfc2217.h
+++ b/src/protocols/serial/rfc2217.h
@@ -58,6 +58,46 @@
 int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,
         guac_serial_settings* settings);

+/**
+ * Opens a base RFC854 Telnet connection to the network serial server described
+ * by the given settings, negotiating only Telnet BINARY/SGA/ECHO options (no
+ * RFC2217 COM-PORT-OPTION), and stores the resulting socket and libtelnet
+ * session in the given stream. Serial line parameters are not negotiated and
+ * remain purely informational, as with the raw transport. Works in both forward
+ * and reverse mode, as the socket is obtained via guac_serial_net_open_fd().
+ *
+ * If Telnet support was not compiled in, the client is aborted and -1 is
+ * returned.
+ *
+ * @param stream
+ *     The serial stream to populate with the opened socket and session.
+ *
+ * @param client
+ *     The client for which the connection is being opened.
+ *
+ * @param settings
+ *     The parsed connection settings describing the server to connect to.
+ *
+ * @return
+ *     Zero on success, non-zero on failure.
+ */
+int guac_serial_telnet_open(guac_serial_stream* stream, guac_client* client,
+        guac_serial_settings* settings);
+
+/**
+ * Sends a serial break to the remote end of a base Telnet connection using the
+ * Telnet BREAK (BRK) command. Plain Telnet has no COM-PORT-OPTION break, so the
+ * NVT BREAK function is used instead; its effect depends on the remote end. This
+ * must only be called on a stream whose backend is GUAC_SERIAL_BACKEND_TELNET.
+ *
+ * @param stream
+ *     The Telnet serial stream on which to send the break.
+ *
+ * @return
+ *     Zero on success, non-zero on error.
+ */
+int guac_serial_telnet_send_break(guac_serial_stream* stream);
+
 /**
  * Sends the given buffer to the remote end of an RFC2217 connection, applying
  * telnet IAC framing. This must only be called on a stream whose backend is
diff --git a/src/protocols/serial/serial.c b/src/protocols/serial/serial.c
index 448881fa..23316d85 100644
--- a/src/protocols/serial/serial.c
+++ b/src/protocols/serial/serial.c
@@ -333,8 +333,10 @@ static guac_serial_disconnect_reason guac_serial_run_read_loop(
             return GUAC_SERIAL_DISCONNECT_ERROR;
         }

-        /* RFC2217 output must be de-framed; all other transports are raw */
-        if (serial_client->stream->backend == GUAC_SERIAL_BACKEND_RFC2217)
+        /* RFC2217 and telnet output must be de-framed; all other transports are
+         * raw */
+        if (serial_client->stream->backend == GUAC_SERIAL_BACKEND_RFC2217
+                || serial_client->stream->backend == GUAC_SERIAL_BACKEND_TELNET)
             guac_serial_rfc2217_recv(serial_client->stream, buffer, bytes_read);
         else
             guac_terminal_write(serial_client->term, buffer, bytes_read);
diff --git a/src/protocols/serial/settings.c b/src/protocols/serial/settings.c
index 1a63d5dd..8a6aa9f1 100644
--- a/src/protocols/serial/settings.c
+++ b/src/protocols/serial/settings.c
@@ -388,6 +388,7 @@ static int guac_serial_parse_network_protocol(const char* str,
         guac_serial_network_protocol* protocol) {
     if (strcmp(str, "raw") == 0)     { *protocol = GUAC_SERIAL_NETWORK_PROTOCOL_RAW;     return 0; }
     if (strcmp(str, "rfc2217") == 0) { *protocol = GUAC_SERIAL_NETWORK_PROTOCOL_RFC2217; return 0; }
+    if (strcmp(str, "telnet") == 0)  { *protocol = GUAC_SERIAL_NETWORK_PROTOCOL_TELNET;  return 0; }
     return 1;
 }

@@ -526,7 +527,7 @@ guac_serial_settings* guac_serial_parse_args(guac_user* user,
             argv, IDX_NETWORK_PROTOCOL, "raw");
     if (guac_serial_parse_network_protocol(proto_str, &settings->network_protocol)) {
         guac_user_log(user, GUAC_LOG_ERROR, "Invalid network-protocol \"%s\": "
-                "must be \"raw\" or \"rfc2217\".", proto_str);
+                "must be \"raw\", \"rfc2217\", or \"telnet\".", proto_str);
         guac_mem_free(proto_str);
         guac_mem_free(settings);
         return NULL;
@@ -603,11 +604,12 @@ guac_serial_settings* guac_serial_parse_args(guac_user* user,
     }

 #ifndef ENABLE_SERIAL_RFC2217
-    /* Reject RFC2217 if libtelnet support was not compiled in */
+    /* Reject RFC2217/telnet if libtelnet support was not compiled in */
     if (settings->type == GUAC_SERIAL_TYPE_NETWORK
-            && settings->network_protocol == GUAC_SERIAL_NETWORK_PROTOCOL_RFC2217) {
-        guac_user_log(user, GUAC_LOG_ERROR, "RFC2217 transport requested but "
-                "not compiled in; rebuild with libtelnet, or use "
+            && (settings->network_protocol == GUAC_SERIAL_NETWORK_PROTOCOL_RFC2217
+                || settings->network_protocol == GUAC_SERIAL_NETWORK_PROTOCOL_TELNET)) {
+        guac_user_log(user, GUAC_LOG_ERROR, "RFC2217/telnet transport requested "
+                "but not compiled in; rebuild with libtelnet, or use "
                 "network-protocol=raw.");
         guac_serial_settings_free(settings);
         return NULL;
diff --git a/src/protocols/serial/settings.h b/src/protocols/serial/settings.h
index f221be0a..65c5ba92 100644
--- a/src/protocols/serial/settings.h
+++ b/src/protocols/serial/settings.h
@@ -106,7 +106,16 @@ typedef enum guac_serial_network_protocol {
      * The RFC2217 (telnet COM-PORT-OPTION) protocol, allowing serial line
      * parameters and break signalling to be negotiated with the remote end.
      */
-    GUAC_SERIAL_NETWORK_PROTOCOL_RFC2217
+    GUAC_SERIAL_NETWORK_PROTOCOL_RFC2217,
+
+    /**
+     * Base RFC854 Telnet framing (IAC escaping plus BINARY/SGA/ECHO option
+     * negotiation) without the RFC2217 COM-PORT-OPTION. Suitable for endpoints
+     * that speak plain Telnet (e.g. QEMU/VMware telnet serial ports, telnet-mode
+     * console servers). Serial line parameters cannot be negotiated and are
+     * purely informational, exactly as with the raw protocol.
+     */
+    GUAC_SERIAL_NETWORK_PROTOCOL_TELNET

 } guac_serial_network_protocol;

diff --git a/src/protocols/serial/stream.c b/src/protocols/serial/stream.c
index cd1c3c0e..c2db391b 100644
--- a/src/protocols/serial/stream.c
+++ b/src/protocols/serial/stream.c
@@ -138,6 +138,8 @@ int guac_serial_stream_reopen(guac_serial_stream* stream, guac_client* client,
         result = guac_serial_local_open(stream, client, settings);
     else if (settings->network_protocol == GUAC_SERIAL_NETWORK_PROTOCOL_RFC2217)
         result = guac_serial_rfc2217_open(stream, client, settings);
+    else if (settings->network_protocol == GUAC_SERIAL_NETWORK_PROTOCOL_TELNET)
+        result = guac_serial_telnet_open(stream, client, settings);
     else
         result = guac_serial_tcp_open(stream, client, settings);

@@ -172,7 +174,8 @@ int guac_serial_stream_write(guac_serial_stream* stream, const char* buffer,
             if (stream->client->state != GUAC_CLIENT_RUNNING)
                 break;

-            if (stream->backend == GUAC_SERIAL_BACKEND_RFC2217)
+            if (stream->backend == GUAC_SERIAL_BACKEND_RFC2217
+                    || stream->backend == GUAC_SERIAL_BACKEND_TELNET)
                 guac_serial_rfc2217_send(stream, buffer + i, 1);
             else if (guac_serial_stream_write_all(stream->fd, buffer + i, 1) != 1)
                 /* The descriptor went bad; the read loop will detect the drop
@@ -188,7 +191,8 @@ int guac_serial_stream_write(guac_serial_stream* stream, const char* buffer,
     /* Otherwise, write the entire buffer at once */
     else {

-        if (stream->backend == GUAC_SERIAL_BACKEND_RFC2217)
+        if (stream->backend == GUAC_SERIAL_BACKEND_RFC2217
+                || stream->backend == GUAC_SERIAL_BACKEND_TELNET)
             guac_serial_rfc2217_send(stream, buffer, length);
         else
             guac_serial_stream_write_all(stream->fd, buffer, length);
@@ -241,6 +245,10 @@ int guac_serial_stream_send_break(guac_serial_stream* stream) {
             guac_serial_rfc2217_send_break(stream);
             break;

+        case GUAC_SERIAL_BACKEND_TELNET:
+            guac_serial_telnet_send_break(stream);
+            break;
+
     }

     pthread_mutex_unlock(&stream->write_lock);
@@ -295,6 +303,12 @@ int guac_serial_stream_set_line(guac_serial_stream* stream,
             break;
         }

+        case GUAC_SERIAL_BACKEND_TELNET:
+            guac_client_log(stream->client, GUAC_LOG_WARNING, "Control-line "
+                    "toggles (DTR/RTS) require the rfc2217 transport; ignoring "
+                    "on telnet transport.");
+            break;
+
     }

     pthread_mutex_unlock(&stream->write_lock);
diff --git a/src/protocols/serial/stream.h b/src/protocols/serial/stream.h
index 339119bb..e488aecb 100644
--- a/src/protocols/serial/stream.h
+++ b/src/protocols/serial/stream.h
@@ -45,7 +45,13 @@ typedef enum guac_serial_backend {
     /**
      * An RFC2217 (telnet COM-PORT-OPTION) connection to a remote serial server.
      */
-    GUAC_SERIAL_BACKEND_RFC2217
+    GUAC_SERIAL_BACKEND_RFC2217,
+
+    /**
+     * A base RFC854 Telnet connection to a remote serial server, using Telnet
+     * IAC framing without the RFC2217 COM-PORT-OPTION.
+     */
+    GUAC_SERIAL_BACKEND_TELNET

 } guac_serial_backend;