Commit bc1c454e for guacamole.apache.org
commit bc1c454e92bdf51e03518412e39139907879dfa2
Author: Ciro Iriarte <ciro.iriarte@gmail.com>
Date: Fri Jul 17 12:12:15 2026 -0300
GUACAMOLE-2301: Add serial reverse (listen) mode for inbound connections
Add a reverse/listen mode for network serial connections: instead of
dialing out to a gateway, guacd binds a local TCP port and accepts a
single inbound connection from the device (e.g. a QEMU/KVM or VMware
virtual serial port, or a console server configured to dial out).
Network fd acquisition is factored into guac_serial_net_open_fd(), used
by both the raw and RFC2217 backends, which either dials out or, when
reverse-connect is set, binds/listens/accepts. The accepted socket feeds
the existing raw/RFC2217 framing unchanged, and the auto-reconnect path
re-listens for the next inbound connection on drop.
New parameters: reverse-connect, bind-address (default 127.0.0.1),
listen-timeout (default 60s). In reverse mode hostname is optional.
diff --git a/src/protocols/serial/rfc2217.c b/src/protocols/serial/rfc2217.c
index e3ba18d9..c7dfc333 100644
--- a/src/protocols/serial/rfc2217.c
+++ b/src/protocols/serial/rfc2217.c
@@ -22,6 +22,7 @@
#include "rfc2217.h"
#include "settings.h"
#include "stream.h"
+#include "tcp.h"
#include <guacamole/client.h>
#include <guacamole/protocol.h>
@@ -239,40 +240,11 @@ int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,
{ -1, 0, 0 }
};
- /* Connect to the remote serial server */
- int fd = guac_tcp_connect(settings->hostname, settings->port,
- settings->timeout);
- if (fd < 0) {
-
- int err = errno;
-
- /* Distinguish an actively-refused or reset endpoint from other
- * failures. The client is not aborted here so the caller may retry. */
- if (err == ECONNREFUSED) {
- guac_client_log(client, GUAC_LOG_ERROR, "ser2net endpoint "
- "\"%s:%s\" refused the connection: %s", settings->hostname,
- settings->port, strerror(err));
- stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
- }
- else if (err == ECONNRESET) {
- guac_client_log(client, GUAC_LOG_ERROR, "Connection to ser2net "
- "endpoint \"%s:%s\" was reset: %s", settings->hostname,
- settings->port, strerror(err));
- stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
- }
- else {
- guac_client_log(client, GUAC_LOG_ERROR, "Unable to connect to "
- "serial server \"%s\" port \"%s\".", settings->hostname,
- settings->port);
- stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND;
- }
-
+ /* 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;
- }
-
- /* Disable Nagle's algorithm so keystrokes are sent without delay */
- int flag = 1;
- setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
/* Store the socket before initializing libtelnet, as the event handler
* writes outbound data to this descriptor */
diff --git a/src/protocols/serial/serial.c b/src/protocols/serial/serial.c
index ff8f1b93..448881fa 100644
--- a/src/protocols/serial/serial.c
+++ b/src/protocols/serial/serial.c
@@ -116,6 +116,8 @@ static void guac_serial_endpoint(guac_serial_settings* settings, char* buffer,
int size) {
if (settings->type == GUAC_SERIAL_TYPE_LOCAL)
snprintf(buffer, size, "%s", settings->device);
+ else if (settings->reverse_connect)
+ snprintf(buffer, size, "listen:%s", settings->port);
else
snprintf(buffer, size, "%s:%s", settings->hostname, settings->port);
}
@@ -439,9 +441,15 @@ void* guac_serial_client_thread(void* data) {
settings->baud_rate, settings->data_bits,
guac_serial_parity_letter(settings->parity), settings->stop_bits);
- /* Set process title to the device or hostname reached by this connection */
- const char* title_host = settings->type == GUAC_SERIAL_TYPE_LOCAL
- ? settings->device : settings->hostname;
+ /* Set process title to the device, listen endpoint, or hostname reached by
+ * this connection */
+ const char* title_host;
+ if (settings->type == GUAC_SERIAL_TYPE_LOCAL)
+ title_host = settings->device;
+ else if (settings->reverse_connect)
+ title_host = endpoint;
+ else
+ title_host = settings->hostname;
guac_process_title_set_endpoint(GUAC_SERIAL_PROCESS_TITLE_NAME,
NULL, title_host, NULL);
diff --git a/src/protocols/serial/settings.c b/src/protocols/serial/settings.c
index ddb68cc8..1a63d5dd 100644
--- a/src/protocols/serial/settings.c
+++ b/src/protocols/serial/settings.c
@@ -78,6 +78,9 @@ const char* GUAC_SERIAL_CLIENT_ARGS[] = {
"line-ending",
"local-echo",
"hangup-on-close",
+ "reverse-connect",
+ "listen-timeout",
+ "bind-address",
NULL
};
@@ -310,6 +313,24 @@ enum SERIAL_ARGS_IDX {
*/
IDX_HANGUP_ON_CLOSE,
+ /**
+ * Whether guacd should listen for an inbound connection from the device
+ * (reverse mode) instead of dialing out. Defaults to false.
+ */
+ IDX_REVERSE_CONNECT,
+
+ /**
+ * The number of seconds to wait for an inbound connection in reverse mode
+ * before failing. Zero or negative means wait indefinitely.
+ */
+ IDX_LISTEN_TIMEOUT,
+
+ /**
+ * The local address to bind when listening in reverse mode. Defaults to
+ * "127.0.0.1".
+ */
+ IDX_BIND_ADDRESS,
+
SERIAL_ARGS_COUNT
};
@@ -530,6 +551,30 @@ guac_serial_settings* guac_serial_parse_args(guac_user* user,
guac_user_parse_args_int(user, GUAC_SERIAL_CLIENT_ARGS, argv,
IDX_TIMEOUT, GUAC_SERIAL_DEFAULT_TIMEOUT);
+ /* Parse reverse (listen) mode flag */
+ settings->reverse_connect =
+ guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_REVERSE_CONNECT, false);
+
+ /* Parse listen timeout, treating negative values as "wait indefinitely" */
+ settings->listen_timeout =
+ guac_user_parse_args_int(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_LISTEN_TIMEOUT, GUAC_SERIAL_DEFAULT_LISTEN_TIMEOUT);
+ if (settings->listen_timeout < 0)
+ settings->listen_timeout = 0;
+
+ /* Parse local bind address used when listening in reverse mode */
+ settings->bind_address =
+ guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+ IDX_BIND_ADDRESS, "127.0.0.1");
+
+ /* Reverse mode is meaningless for local connections */
+ if (settings->reverse_connect && settings->type == GUAC_SERIAL_TYPE_LOCAL) {
+ guac_user_log(user, GUAC_LOG_WARNING, "reverse-connect has no effect on "
+ "local serial connections; ignoring.");
+ settings->reverse_connect = false;
+ }
+
/* Validate transport-specific requirements */
if (settings->type == GUAC_SERIAL_TYPE_LOCAL) {
if (settings->device == NULL || settings->device[0] == '\0') {
@@ -540,7 +585,10 @@ guac_serial_settings* guac_serial_parse_args(guac_user* user,
}
}
else {
- if (settings->hostname == NULL || settings->hostname[0] == '\0') {
+ /* In reverse mode guacd binds and listens rather than dialing out, so
+ * a hostname is only required when dialing out */
+ if (!settings->reverse_connect
+ && (settings->hostname == NULL || settings->hostname[0] == '\0')) {
guac_user_log(user, GUAC_LOG_ERROR, "A \"hostname\" is required for "
"network serial connections.");
guac_serial_settings_free(settings);
@@ -827,6 +875,7 @@ void guac_serial_settings_free(guac_serial_settings* settings) {
guac_mem_free(settings->allowed_devices);
guac_mem_free(settings->hostname);
guac_mem_free(settings->port);
+ guac_mem_free(settings->bind_address);
/* Free display preferences */
guac_mem_free(settings->font_name);
diff --git a/src/protocols/serial/settings.h b/src/protocols/serial/settings.h
index 65dab32e..f221be0a 100644
--- a/src/protocols/serial/settings.h
+++ b/src/protocols/serial/settings.h
@@ -50,6 +50,12 @@
*/
#define GUAC_SERIAL_DEFAULT_BREAK_DURATION 500
+/**
+ * The default number of seconds to wait for an inbound connection in reverse
+ * mode before failing.
+ */
+#define GUAC_SERIAL_DEFAULT_LISTEN_TIMEOUT 60
+
/**
* The interval, in seconds, between automatic reconnection attempts after the
* serial connection is dropped.
@@ -192,6 +198,24 @@ typedef struct guac_serial_settings {
*/
char* port;
+ /**
+ * Whether guacd should listen for an inbound connection from the device
+ * (reverse mode) instead of dialing out. Only meaningful for network type.
+ */
+ bool reverse_connect;
+
+ /**
+ * Seconds to wait for an inbound connection in reverse mode before failing.
+ * Zero or negative means wait indefinitely (bounded by client shutdown).
+ */
+ int listen_timeout;
+
+ /**
+ * Local address to bind when listening in reverse mode. NULL/empty =>
+ * 127.0.0.1.
+ */
+ char* bind_address;
+
/**
* The number of seconds to wait for a connection before timing out.
*/
diff --git a/src/protocols/serial/tcp.c b/src/protocols/serial/tcp.c
index a45594a0..6b73f973 100644
--- a/src/protocols/serial/tcp.c
+++ b/src/protocols/serial/tcp.c
@@ -24,52 +24,242 @@
#include "tcp.h"
#include <guacamole/client.h>
+#include <guacamole/error.h>
#include <guacamole/protocol.h>
#include <guacamole/tcp.h>
#include <errno.h>
+#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
+#include <poll.h>
#include <string.h>
+#include <sys/socket.h>
+#include <unistd.h>
-int guac_serial_tcp_open(guac_serial_stream* stream, guac_client* client,
- guac_serial_settings* settings) {
+/**
+ * Binds and listens on the address/port described by the given settings and
+ * accepts a single inbound connection from the device (reverse mode). The wait
+ * for the inbound connection honors both the configured listen timeout and
+ * client shutdown. On success the accepted socket is returned; TCP_NODELAY is
+ * left to the caller (guac_serial_net_open_fd).
+ *
+ * @param stream
+ * The serial stream whose open_status is set on failure.
+ *
+ * @param client
+ * The client for which the connection is being opened.
+ *
+ * @param settings
+ * The parsed connection settings describing the address/port to listen on.
+ *
+ * @return
+ * The accepted file descriptor on success, or -1 on failure with
+ * stream->open_status set and the reason logged.
+ */
+static int guac_serial_reverse_accept(guac_serial_stream* stream,
+ guac_client* client, guac_serial_settings* settings) {
- /* Connect to the remote serial server */
- int fd = guac_tcp_connect(settings->hostname, settings->port,
- settings->timeout);
- if (fd < 0) {
+ /* Default to loopback if no bind address was configured */
+ const char* bind_addr = (settings->bind_address && settings->bind_address[0])
+ ? settings->bind_address : "127.0.0.1";
- int err = errno;
+ /* Resolve the local address to bind and listen on */
+ struct addrinfo hints = {
+ .ai_family = AF_UNSPEC,
+ .ai_socktype = SOCK_STREAM,
+ .ai_flags = AI_PASSIVE
+ };
- /* Distinguish an actively-refused or reset endpoint from other
- * failures (e.g. name resolution, timeout) */
- if (err == ECONNREFUSED) {
- guac_client_log(client, GUAC_LOG_ERROR, "ser2net endpoint "
- "\"%s:%s\" refused the connection: %s", settings->hostname,
- settings->port, strerror(err));
- stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
- }
- else if (err == ECONNRESET) {
- guac_client_log(client, GUAC_LOG_ERROR, "Connection to ser2net "
- "endpoint \"%s:%s\" was reset: %s", settings->hostname,
- settings->port, strerror(err));
- stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
+ struct addrinfo* addrs;
+ int err = getaddrinfo(bind_addr, settings->port, &hints, &addrs);
+ if (err != 0) {
+ guac_client_log(client, GUAC_LOG_ERROR, "Unable to resolve bind "
+ "address \"%s\" for reverse serial connection: %s", bind_addr,
+ gai_strerror(err));
+ stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
+ return -1;
+ }
+
+ /* Bind to the first address for which a socket can be created and bound */
+ int listen_fd = -1;
+ for (struct addrinfo* addr = addrs; addr != NULL; addr = addr->ai_next) {
+
+ listen_fd = socket(addr->ai_family,
+ addr->ai_socktype | SOCK_CLOEXEC, addr->ai_protocol);
+ if (listen_fd < 0)
+ continue;
+
+ /* Allow re-listening after a reconnect despite lingering TIME_WAIT */
+ int reuse = 1;
+ setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
+
+ if (bind(listen_fd, addr->ai_addr, addr->ai_addrlen) == 0)
+ break;
+
+ close(listen_fd);
+ listen_fd = -1;
+
+ }
+
+ freeaddrinfo(addrs);
+
+ if (listen_fd < 0) {
+ guac_client_log(client, GUAC_LOG_ERROR, "Unable to bind %s:%s for "
+ "reverse serial connection: %s", bind_addr, settings->port,
+ strerror(errno));
+ stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
+ return -1;
+ }
+
+ /* Listen for a single inbound connection */
+ if (listen(listen_fd, 1) != 0) {
+ guac_client_log(client, GUAC_LOG_ERROR, "Unable to listen on %s:%s for "
+ "reverse serial connection: %s", bind_addr, settings->port,
+ strerror(errno));
+ close(listen_fd);
+ stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
+ return -1;
+ }
+
+ guac_client_log(client, GUAC_LOG_INFO, "Listening on %s:%s for an inbound "
+ "serial connection...", bind_addr, settings->port);
+
+ /* Wait for one inbound connection, honoring both the listen timeout and
+ * client shutdown. Poll in 1000ms slices so teardown stays responsive. */
+ int timeout = settings->listen_timeout; /* seconds; <= 0 waits forever */
+ int waited_ms = 0;
+ while (client->state == GUAC_CLIENT_RUNNING) {
+
+ struct pollfd pfd = { .fd = listen_fd, .events = POLLIN };
+ int pr = poll(&pfd, 1, 1000);
+ if (pr < 0) {
+ if (errno == EINTR)
+ continue;
+ guac_client_log(client, GUAC_LOG_WARNING, "Error while waiting for "
+ "an inbound serial connection on %s:%s: %s", bind_addr,
+ settings->port, strerror(errno));
+ break;
}
- else {
- guac_client_log(client, GUAC_LOG_ERROR, "Unable to connect to "
- "serial server \"%s\" port \"%s\".", settings->hostname,
- settings->port);
- stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND;
+
+ /* An inbound connection is ready to accept */
+ if (pr > 0 && (pfd.revents & POLLIN))
+ break;
+
+ /* Otherwise a slice elapsed; enforce the listen timeout if configured */
+ if (timeout > 0) {
+ waited_ms += 1000;
+ if (waited_ms >= timeout * 1000) {
+ guac_client_log(client, GUAC_LOG_ERROR, "Timed out after %ds "
+ "waiting for an inbound serial connection on %s:%s",
+ timeout, bind_addr, settings->port);
+ close(listen_fd);
+ stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_TIMEOUT;
+ return -1;
+ }
}
+ }
+
+ /* Abort if the client is shutting down rather than accepting */
+ if (client->state != GUAC_CLIENT_RUNNING) {
+ close(listen_fd);
+ stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
+ return -1;
+ }
+
+ /* Accept the single inbound connection and stop listening */
+ struct sockaddr_storage peer;
+ socklen_t plen = sizeof(peer);
+ int fd;
+ GUAC_RETRY_EINTR(fd, accept(listen_fd, (struct sockaddr*) &peer, &plen));
+ close(listen_fd);
+
+ if (fd < 0) {
+ guac_client_log(client, GUAC_LOG_ERROR, "Failed to accept inbound "
+ "serial connection on %s:%s: %s", bind_addr, settings->port,
+ strerror(errno));
+ stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
return -1;
}
+ /* Log the peer which connected */
+ char host[NI_MAXHOST] = "?";
+ char serv[NI_MAXSERV] = "?";
+ getnameinfo((struct sockaddr*) &peer, plen, host, sizeof(host),
+ serv, sizeof(serv), NI_NUMERICHOST | NI_NUMERICSERV);
+ guac_client_log(client, GUAC_LOG_INFO, "Accepted inbound serial connection "
+ "from %s:%s", host, serv);
+
+ /* Keep an idle inbound link alive so a silent device drop is detected */
+ int keepalive = 1;
+ setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
+
+ return fd;
+
+}
+
+int guac_serial_net_open_fd(guac_serial_stream* stream, guac_client* client,
+ guac_serial_settings* settings) {
+
+ int fd;
+
+ /* In reverse mode, accept an inbound connection rather than dialing out.
+ * guac_serial_reverse_accept() already logs and sets open_status. */
+ if (settings->reverse_connect) {
+ fd = guac_serial_reverse_accept(stream, client, settings);
+ if (fd < 0)
+ return -1;
+ }
+
+ /* Otherwise dial out to the remote serial server */
+ else {
+ fd = guac_tcp_connect(settings->hostname, settings->port,
+ settings->timeout);
+ if (fd < 0) {
+
+ int err = errno;
+
+ /* Distinguish an actively-refused or reset endpoint from other
+ * failures (e.g. name resolution, timeout) */
+ if (err == ECONNREFUSED) {
+ guac_client_log(client, GUAC_LOG_ERROR, "ser2net endpoint "
+ "\"%s:%s\" refused the connection: %s",
+ settings->hostname, settings->port, strerror(err));
+ stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
+ }
+ else if (err == ECONNRESET) {
+ guac_client_log(client, GUAC_LOG_ERROR, "Connection to ser2net "
+ "endpoint \"%s:%s\" was reset: %s", settings->hostname,
+ settings->port, strerror(err));
+ stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
+ }
+ else {
+ guac_client_log(client, GUAC_LOG_ERROR, "Unable to connect to "
+ "serial server \"%s\" port \"%s\".", settings->hostname,
+ settings->port);
+ stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND;
+ }
+
+ return -1;
+ }
+ }
+
/* Disable Nagle's algorithm so keystrokes are sent without delay */
int flag = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
+ return fd;
+
+}
+
+int guac_serial_tcp_open(guac_serial_stream* stream, guac_client* client,
+ guac_serial_settings* settings) {
+
+ int fd = guac_serial_net_open_fd(stream, client, settings);
+ if (fd < 0)
+ return -1;
+
stream->backend = GUAC_SERIAL_BACKEND_TCP;
stream->fd = fd;
return 0;
diff --git a/src/protocols/serial/tcp.h b/src/protocols/serial/tcp.h
index 638dcefa..c1e182a9 100644
--- a/src/protocols/serial/tcp.h
+++ b/src/protocols/serial/tcp.h
@@ -45,4 +45,29 @@
int guac_serial_tcp_open(guac_serial_stream* stream, guac_client* client,
guac_serial_settings* settings);
+/**
+ * Acquires the network file descriptor for a serial connection. In reverse mode
+ * (settings->reverse_connect) guacd binds, listens, and accepts a single
+ * inbound connection from the device; otherwise it dials out via
+ * guac_tcp_connect(). TCP_NODELAY is set on the resulting socket on success.
+ * This is shared by both the raw TCP and RFC2217 backends so that reverse mode
+ * need only be implemented once.
+ *
+ * @param stream
+ * The serial stream whose open_status is set on failure.
+ *
+ * @param client
+ * The client for which the connection is being opened.
+ *
+ * @param settings
+ * The parsed connection settings describing the server to connect to or the
+ * address/port to listen on.
+ *
+ * @return
+ * The acquired file descriptor on success, or -1 on failure with
+ * stream->open_status set and the reason logged.
+ */
+int guac_serial_net_open_fd(guac_serial_stream* stream, guac_client* client,
+ guac_serial_settings* settings);
+
#endif