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;