Commit f4bbb4bb for guacamole.apache.org

commit f4bbb4bb1dd0170b0032965110d8b21dd9f78380
Author: Ciro Iriarte <ciro.iriarte@gmail.com>
Date:   Sat Oct 31 15:24:35 2026 -0300

    GUACAMOLE-2301: Address review feedback on serial protocol support

    Style and documentation:
    - Put each switch case on its own lines and expand one-line conditionals.
    - Complete function documentation, document every parity and flow-control
      enum member, and note the default of each connection argument.
    - Correct docs that omitted the Telnet transport or claimed that open
      failures abort the client; document the dtr/rts control-pipe commands.
    - Convert the open() and connect() errno chains to switch statements.

    libtelnet / build:
    - Drop the TELNET_BREAK fallback (libtelnet defines it) and rename the
      COM-PORT-OPTION number to GUAC_SERIAL_TELOPT_COMPORT, since libtelnet
      does not define option 44.
    - Define ENABLE_SERIAL_RFC2217 from the existing ENABLE_TELNET conditional
      rather than a separate one, report build_telnet in the configure
      summary, and align the summary columns.

    Settings:
    - Parse transport-specific arguments only for the transport in use.
    - Remove the per-connection allowed-devices parameter: whoever can set
      "device" can also set the allowlist, so it provides no protection. A
      guacd-level allowlist can be proposed separately.
    - Remove the unused GUAC_SERIAL_DEFAULT_PORT.

diff --git a/configure.ac b/configure.ac
index 22948941..b32bf635 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1387,17 +1387,16 @@ AM_CONDITIONAL([ENABLE_TELNET], [test "x${have_libtelnet}"  = "xyes" \
                                    -a "x${have_terminal}" = "xyes"])

 # Serial console support. The core plugin (local devices and raw TCP) requires
-# only the terminal. The RFC2217 network transport additionally requires
-# libtelnet and is compiled in conditionally (see ENABLE_SERIAL_RFC2217).
+# only the terminal. The RFC2217 and Telnet network transports additionally
+# require libtelnet, and are compiled in only when Telnet support itself is
+# being built (ENABLE_TELNET), in which case ENABLE_SERIAL_RFC2217 is defined.
+# The serial plugin already requires the terminal, so this is equivalent to
+# libtelnet being available.
 AM_CONDITIONAL([ENABLE_SERIAL], [test "x${have_terminal}" = "xyes"])
-
-# RFC2217 (COM-PORT-OPTION) serial transport, built on libtelnet. When
-# libtelnet is unavailable the serial plugin still builds; only the RFC2217
-# network transport is omitted.
-AM_CONDITIONAL([ENABLE_SERIAL_RFC2217], [test "x${have_libtelnet}" = "xyes"])
-AS_IF([test "x${have_libtelnet}" = "xyes"],
-      [AC_DEFINE([ENABLE_SERIAL_RFC2217], [1],
-                 [Define if RFC2217 serial transport (libtelnet) is available])])
+AM_COND_IF([ENABLE_TELNET],
+           [AC_DEFINE([ENABLE_SERIAL_RFC2217], [1],
+                      [Define if the RFC2217/Telnet serial transports
+                       are available])])

 AC_SUBST(TELNET_LIBS)

@@ -1657,13 +1656,13 @@ $PACKAGE_NAME version $PACKAGE_VERSION

    Protocol support:

-      Kubernetes .... ${build_kubernetes}
-      RDP ........... ${build_rdp}
-      SSH ........... ${build_ssh}
-      Serial ........ ${build_serial}
-      RFC2217 (serial) .... ${have_libtelnet}
-      Telnet ........ ${build_telnet}
-      VNC ........... ${build_vnc}
+      Kubernetes .......... ${build_kubernetes}
+      RDP ................. ${build_rdp}
+      SSH ................. ${build_ssh}
+      Serial .............. ${build_serial}
+      RFC2217 (serial) .... ${build_telnet}
+      Telnet .............. ${build_telnet}
+      VNC ................. ${build_vnc}

    Services / tools:

diff --git a/src/protocols/serial/local.c b/src/protocols/serial/local.c
index 3eb9b2c4..9eff0456 100644
--- a/src/protocols/serial/local.c
+++ b/src/protocols/serial/local.c
@@ -36,39 +36,35 @@
 int guac_serial_local_open(guac_serial_stream* stream, guac_client* client,
         guac_serial_settings* settings) {

-    /* Re-validate the device against the allowlist on every (re)open, guarding
-     * against a symlink being repointed at a disallowed device between
-     * reconnects */
-    if (settings->allowed_devices != NULL && settings->allowed_devices[0] != '\0'
-            && !guac_serial_device_permitted(settings->device,
-                    settings->allowed_devices)) {
-        guac_client_log(client, GUAC_LOG_ERROR, "Serial device \"%s\" is not "
-                "permitted by the allowed-devices list, or is not currently "
-                "available.", settings->device);
-        stream->open_status = GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN;
-        return -1;
-    }
-
     /* Open device non-blocking so open() does not wait for carrier detect */
     int fd = open(settings->device, O_RDWR | O_NOCTTY | O_NONBLOCK);
     if (fd < 0) {

         /* Distinguish contention from other failures */
-        if (errno == EBUSY || errno == EACCES || errno == EPERM) {
-            guac_client_log(client, GUAC_LOG_ERROR, "Serial device \"%s\" is "
-                    "busy or exclusively locked by another process: %s",
-                    settings->device, strerror(errno));
-            stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
-        }
-        else if (errno == ENOENT) {
-            guac_client_log(client, GUAC_LOG_ERROR, "Serial device \"%s\" not "
-                    "found: %s", settings->device, strerror(errno));
-            stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND;
-        }
-        else {
-            guac_client_log(client, GUAC_LOG_ERROR, "Unable to open serial "
-                    "device \"%s\": %s", settings->device, strerror(errno));
-            stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
+        switch (errno) {
+
+            case EBUSY:
+            case EACCES:
+            case EPERM:
+                guac_client_log(client, GUAC_LOG_ERROR, "Serial device \"%s\" "
+                        "is busy or exclusively locked by another process: "
+                        "%s", settings->device, strerror(errno));
+                stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE;
+                break;
+
+            case ENOENT:
+                guac_client_log(client, GUAC_LOG_ERROR, "Serial device \"%s\" "
+                        "not found: %s", settings->device, strerror(errno));
+                stream->open_status = GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND;
+                break;
+
+            default:
+                guac_client_log(client, GUAC_LOG_ERROR, "Unable to open "
+                        "serial device \"%s\": %s", settings->device,
+                        strerror(errno));
+                stream->open_status = GUAC_PROTOCOL_STATUS_SERVER_ERROR;
+                break;
+
         }

         return -1;
@@ -115,10 +111,23 @@ int guac_serial_local_open(guac_serial_stream* stream, guac_client* client,
     /* Apply data bits */
     tio.c_cflag &= ~CSIZE;
     switch (settings->data_bits) {
-        case 5: tio.c_cflag |= CS5; break;
-        case 6: tio.c_cflag |= CS6; break;
-        case 7: tio.c_cflag |= CS7; break;
-        default: tio.c_cflag |= CS8; break;
+
+        case 5:
+            tio.c_cflag |= CS5;
+            break;
+
+        case 6:
+            tio.c_cflag |= CS6;
+            break;
+
+        case 7:
+            tio.c_cflag |= CS7;
+            break;
+
+        default:
+            tio.c_cflag |= CS8;
+            break;
+
     }

     /* Apply parity */
diff --git a/src/protocols/serial/local.h b/src/protocols/serial/local.h
index 40eccc7a..98a0e1ba 100644
--- a/src/protocols/serial/local.h
+++ b/src/protocols/serial/local.h
@@ -29,7 +29,9 @@
  * Opens the local serial device described by the given settings, configuring
  * its termios line discipline (baud rate, data/stop bits, parity, and flow
  * control) for raw I/O, and stores the resulting file descriptor in the given
- * stream. On failure, the client is aborted.
+ * stream. On failure, the
+ * stream's open_status is set and non-zero is returned; whether to retry is
+ * left to the caller.
  *
  * @param stream
  *     The serial stream to populate with the opened device.
diff --git a/src/protocols/serial/pipe.c b/src/protocols/serial/pipe.c
index 0fd59587..3f94c271 100644
--- a/src/protocols/serial/pipe.c
+++ b/src/protocols/serial/pipe.c
@@ -97,8 +97,28 @@ static void guac_serial_control_dispatch(guac_user* user, const char* token) {
 }

 /**
- * Blob handler for the inbound control pipe: reassembles whitespace-delimited
- * plain-text tokens and dispatches each complete token.
+ * Handler for "blob" instructions received on the inbound "serial-control"
+ * pipe stream, as defined by guac_user_blob_handler. Reassembles the received
+ * data into whitespace-delimited plain-text tokens, dispatching each complete
+ * token via guac_serial_control_dispatch(). Tokens which exceed the reassembly
+ * buffer are dropped. Each blob is acknowledged once processed.
+ *
+ * @param user
+ *     The user that sent the blob.
+ *
+ * @param stream
+ *     The control pipe stream on which the blob was received. Its data member
+ *     points to the guac_serial_control_stream holding reassembly state.
+ *
+ * @param data
+ *     The blob data received.
+ *
+ * @param length
+ *     The number of bytes of blob data received.
+ *
+ * @return
+ *     Zero if the blob was handled successfully, non-zero otherwise. This
+ *     handler always returns zero.
  */
 static int guac_serial_control_blob_handler(guac_user* user,
         guac_stream* stream, void* data, int length) {
@@ -138,8 +158,22 @@ static int guac_serial_control_blob_handler(guac_user* user,
 }

 /**
- * End handler for the inbound control pipe. Dispatches any final unterminated
- * token and releases the reassembly state.
+ * Handler for the "end" instruction received on the inbound "serial-control"
+ * pipe stream, as defined by guac_user_end_handler. Dispatches any final token
+ * not terminated by whitespace and releases the reassembly state associated
+ * with the stream.
+ *
+ * @param user
+ *     The user that closed the control pipe.
+ *
+ * @param stream
+ *     The control pipe stream being closed. Its data member points to the
+ *     guac_serial_control_stream holding reassembly state, which is freed by
+ *     this handler.
+ *
+ * @return
+ *     Zero if the end of the stream was handled successfully, non-zero
+ *     otherwise. This handler always returns zero.
  */
 static int guac_serial_control_end_handler(guac_user* user,
         guac_stream* stream) {
diff --git a/src/protocols/serial/pipe.h b/src/protocols/serial/pipe.h
index c6f02cbe..88dd0f4d 100644
--- a/src/protocols/serial/pipe.h
+++ b/src/protocols/serial/pipe.h
@@ -31,8 +31,11 @@
 /**
  * The name reserved for the inbound out-of-band control pipe stream. A client
  * opens an inbound pipe stream with this name to send plain-text, whitespace-
- * delimited control commands to the serial connection. The only command
- * currently supported is "break", which sends a serial break.
+ * delimited control commands to the serial connection. The supported commands
+ * are "break", which sends a serial break, and "dtr-on", "dtr-off",
+ * "rts-on", and "rts-off", which assert or deassert the DTR and RTS control
+ * lines. Control-line commands are honored only by the local and RFC2217
+ * transports; the raw TCP and Telnet transports log a warning and ignore them.
  */
 #define GUAC_SERIAL_CONTROL_PIPE_NAME "serial-control"

diff --git a/src/protocols/serial/rfc2217.c b/src/protocols/serial/rfc2217.c
index 4a061a62..daac58ed 100644
--- a/src/protocols/serial/rfc2217.c
+++ b/src/protocols/serial/rfc2217.c
@@ -47,17 +47,7 @@
  * The telnet option number of the RFC2217 COM-PORT-OPTION. libtelnet does not
  * define this option, so it is defined here.
  */
-#ifndef TELNET_TELOPT_COMPORT
-#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
+#define GUAC_SERIAL_TELOPT_COMPORT 44

 /**
  * COM-PORT-OPTION client-to-server command: set the serial line speed. The
@@ -128,19 +118,39 @@ static int guac_serial_rfc2217_write_all(int fd, const char* buffer, int size) {
 }

 /**
- * Sends a single-byte COM-PORT-OPTION subnegotiation.
+ * Sends a COM-PORT-OPTION subnegotiation consisting of the given command byte
+ * followed by a single data byte, as used by all RFC2217 client-to-server
+ * commands other than SET-BAUDRATE.
+ *
+ * @param telnet
+ *     The libtelnet session over which the subnegotiation should be sent.
+ *
+ * @param command
+ *     The COM-PORT-OPTION command code, such as
+ *     GUAC_SERIAL_RFC2217_SET_DATASIZE.
+ *
+ * @param value
+ *     The single byte of data associated with the command.
  */
 static void guac_serial_rfc2217_send_byte(telnet_t* telnet,
         unsigned char command, unsigned char value) {
-    telnet_begin_sb(telnet, TELNET_TELOPT_COMPORT);
+    telnet_begin_sb(telnet, GUAC_SERIAL_TELOPT_COMPORT);
     telnet_send(telnet, (char*) &command, 1);
     telnet_send(telnet, (char*) &value, 1);
     telnet_finish_sb(telnet);
 }

 /**
- * Sends the serial line parameters described by the given settings using
- * client-to-server COM-PORT-OPTION subnegotiations.
+ * Sends the serial line parameters described by the given settings (baud rate,
+ * data bits, parity, stop bits and flow control) to the remote RFC2217 server
+ * using client-to-server COM-PORT-OPTION subnegotiations.
+ *
+ * @param telnet
+ *     The libtelnet session over which the subnegotiations should be sent.
+ *
+ * @param settings
+ *     The parsed connection settings containing the serial line parameters to
+ *     send.
  */
 static void guac_serial_rfc2217_set_line(telnet_t* telnet,
         guac_serial_settings* settings) {
@@ -154,7 +164,7 @@ static void guac_serial_rfc2217_set_line(telnet_t* telnet,
         (unsigned char) ((baud >>  8) & 0xFF),
         (unsigned char) ( baud        & 0xFF)
     };
-    telnet_begin_sb(telnet, TELNET_TELOPT_COMPORT);
+    telnet_begin_sb(telnet, GUAC_SERIAL_TELOPT_COMPORT);
     telnet_send(telnet, (char*) rate, sizeof(rate));
     telnet_finish_sb(telnet);

@@ -165,11 +175,27 @@ static void guac_serial_rfc2217_set_line(telnet_t* telnet,
     /* SET-PARITY (1=none, 2=odd, 3=even, 4=mark, 5=space) */
     unsigned char parity;
     switch (settings->parity) {
-        case GUAC_SERIAL_PARITY_ODD:   parity = 2; break;
-        case GUAC_SERIAL_PARITY_EVEN:  parity = 3; break;
-        case GUAC_SERIAL_PARITY_MARK:  parity = 4; break;
-        case GUAC_SERIAL_PARITY_SPACE: parity = 5; break;
-        default:                       parity = 1; break;
+
+        case GUAC_SERIAL_PARITY_ODD:
+            parity = 2;
+            break;
+
+        case GUAC_SERIAL_PARITY_EVEN:
+            parity = 3;
+            break;
+
+        case GUAC_SERIAL_PARITY_MARK:
+            parity = 4;
+            break;
+
+        case GUAC_SERIAL_PARITY_SPACE:
+            parity = 5;
+            break;
+
+        default:
+            parity = 1;
+            break;
+
     }
     guac_serial_rfc2217_send_byte(telnet, GUAC_SERIAL_RFC2217_SET_PARITY,
             parity);
@@ -181,9 +207,19 @@ static void guac_serial_rfc2217_set_line(telnet_t* telnet,
     /* SET-CONTROL flow control (1=none, 2=xon/xoff, 3=rts/cts) */
     unsigned char flow;
     switch (settings->flow_control) {
-        case GUAC_SERIAL_FLOW_XON_XOFF: flow = 2; break;
-        case GUAC_SERIAL_FLOW_RTS_CTS:  flow = 3; break;
-        default:                        flow = 1; break;
+
+        case GUAC_SERIAL_FLOW_XON_XOFF:
+            flow = 2;
+            break;
+
+        case GUAC_SERIAL_FLOW_RTS_CTS:
+            flow = 3;
+            break;
+
+        default:
+            flow = 1;
+            break;
+
     }
     guac_serial_rfc2217_send_byte(telnet, GUAC_SERIAL_RFC2217_SET_CONTROL,
             flow);
@@ -192,7 +228,19 @@ static void guac_serial_rfc2217_set_line(telnet_t* telnet,

 /**
  * Event handler, as defined by libtelnet. Invoked for every event fired by the
- * RFC2217 libtelnet session. The user data is the guac_serial_stream.
+ * libtelnet session of either the RFC2217 or the plain Telnet backend.
+ * Received data is written to the terminal, outbound data is written to the
+ * stream's socket, warnings are logged and errors abort the connection.
+ *
+ * @param telnet
+ *     The libtelnet session which fired the event.
+ *
+ * @param event
+ *     The event which was fired.
+ *
+ * @param data
+ *     The user data associated with the libtelnet session, which is the
+ *     guac_serial_stream that owns the session.
  */
 static void guac_serial_rfc2217_event_handler(telnet_t* telnet,
         telnet_event_t* event, void* data) {
@@ -241,7 +289,7 @@ int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,

     /* Support levels for the telnet options used by the RFC2217 session */
     static const telnet_telopt_t rfc2217_options[] = {
-        { TELNET_TELOPT_COMPORT, TELNET_WILL, TELNET_DONT },
+        { GUAC_SERIAL_TELOPT_COMPORT, TELNET_WILL, TELNET_DONT },
         { TELNET_TELOPT_BINARY,  TELNET_WILL, TELNET_DO   },
         { TELNET_TELOPT_SGA,     TELNET_WILL, TELNET_DO   },
         { TELNET_TELOPT_ECHO,    TELNET_WONT, TELNET_DO   },
@@ -274,7 +322,7 @@ int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,
     stream->telnet = telnet;

     /* Advertise our willingness to use COM-PORT-OPTION and binary mode */
-    telnet_negotiate(telnet, TELNET_WILL, TELNET_TELOPT_COMPORT);
+    telnet_negotiate(telnet, TELNET_WILL, GUAC_SERIAL_TELOPT_COMPORT);
     telnet_negotiate(telnet, TELNET_WILL, TELNET_TELOPT_BINARY);
     telnet_negotiate(telnet, TELNET_DO, TELNET_TELOPT_BINARY);

@@ -385,7 +433,14 @@ void guac_serial_rfc2217_free(guac_serial_stream* stream) {
     }
 }

-#else /* ENABLE_SERIAL_RFC2217 */
+#else /* !ENABLE_SERIAL_RFC2217 */
+
+/*
+ * libtelnet is not available, so neither the RFC2217 nor the Telnet transport
+ * is compiled in. The functions below fail (open) or do nothing (all others),
+ * allowing the rest of the plugin to link and run with those transports
+ * unavailable.
+ */

 int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,
         guac_serial_settings* settings) {
diff --git a/src/protocols/serial/rfc2217.h b/src/protocols/serial/rfc2217.h
index 12845ea6..64810acc 100644
--- a/src/protocols/serial/rfc2217.h
+++ b/src/protocols/serial/rfc2217.h
@@ -25,22 +25,16 @@

 #include <guacamole/client.h>

-/*
- * This header intentionally does not depend on libtelnet. All libtelnet usage
- * is confined to rfc2217.c and compiled only when ENABLE_SERIAL_RFC2217 is
- * defined (i.e. libtelnet is available). When it is not, the functions below
- * are still defined, but fail or become no-ops, so the rest of the plugin
- * links and runs (with the RFC2217 transport unavailable).
- */
-
 /**
  * Opens an RFC2217 (telnet COM-PORT-OPTION) connection to the network serial
  * server described by the given settings, negotiates the configured serial line
  * parameters, and stores the resulting socket and libtelnet session in the
- * given stream. On failure, the client is aborted.
+ * given stream. On failure, the
+ * stream's open_status is set and non-zero is returned; whether to retry is
+ * left to the caller.
  *
- * If RFC2217 support was not compiled in, the client is aborted and -1 is
- * returned.
+ * If RFC2217 support was not compiled in, an error is logged, the stream's
+ * open_status is set, and -1 is returned.
  *
  * @param stream
  *     The serial stream to populate with the opened socket and session.
@@ -66,8 +60,8 @@ int guac_serial_rfc2217_open(guac_serial_stream* stream, guac_client* client,
  * 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.
+ * If Telnet support was not compiled in, an error is logged, the stream's
+ * open_status is set, and -1 is returned.
  *
  * @param stream
  *     The serial stream to populate with the opened socket and session.
@@ -99,12 +93,12 @@ int guac_serial_telnet_open(guac_serial_stream* stream, guac_client* client,
 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
- * GUAC_SERIAL_BACKEND_RFC2217.
+ * Sends the given buffer to the remote end of an RFC2217 or Telnet connection,
+ * applying Telnet IAC framing. This must only be called on a stream whose
+ * backend is GUAC_SERIAL_BACKEND_RFC2217 or GUAC_SERIAL_BACKEND_TELNET.
  *
  * @param stream
- *     The RFC2217 serial stream to write to.
+ *     The RFC2217 or Telnet serial stream to write to.
  *
  * @param buffer
  *     The buffer of data to send.
@@ -117,12 +111,12 @@ void guac_serial_rfc2217_send(guac_serial_stream* stream, const char* buffer,

 /**
  * Feeds the given buffer of bytes received from the remote end into the
- * RFC2217 libtelnet parser, which will de-frame the data and write terminal
- * output. This must only be called on a stream whose backend is
- * GUAC_SERIAL_BACKEND_RFC2217.
+ * libtelnet parser, which will de-frame the data and write terminal output.
+ * This must only be called on a stream whose backend is
+ * GUAC_SERIAL_BACKEND_RFC2217 or GUAC_SERIAL_BACKEND_TELNET.
  *
  * @param stream
- *     The RFC2217 serial stream that received the data.
+ *     The RFC2217 or Telnet serial stream that received the data.
  *
  * @param buffer
  *     The buffer of received data.
diff --git a/src/protocols/serial/serial.c b/src/protocols/serial/serial.c
index 23316d85..d4eee827 100644
--- a/src/protocols/serial/serial.c
+++ b/src/protocols/serial/serial.c
@@ -91,11 +91,22 @@ typedef enum guac_serial_disconnect_reason {
  */
 static char guac_serial_parity_letter(guac_serial_parity parity) {
     switch (parity) {
-        case GUAC_SERIAL_PARITY_ODD:   return 'O';
-        case GUAC_SERIAL_PARITY_EVEN:  return 'E';
-        case GUAC_SERIAL_PARITY_MARK:  return 'M';
-        case GUAC_SERIAL_PARITY_SPACE: return 'S';
-        default:                       return 'N';
+
+        case GUAC_SERIAL_PARITY_ODD:
+            return 'O';
+
+        case GUAC_SERIAL_PARITY_EVEN:
+            return 'E';
+
+        case GUAC_SERIAL_PARITY_MARK:
+            return 'M';
+
+        case GUAC_SERIAL_PARITY_SPACE:
+            return 'S';
+
+        default:
+            return 'N';
+
     }
 }

@@ -158,9 +169,22 @@ static void guac_serial_write_banner(guac_terminal* term, const char* text) {
 static void guac_serial_line_terminator(guac_serial_line_ending line_ending,
         const char** terminator, int* length) {
     switch (line_ending) {
-        case GUAC_SERIAL_LINE_ENDING_LF:   *terminator = "\n";   *length = 1; break;
-        case GUAC_SERIAL_LINE_ENDING_CRLF: *terminator = "\r\n"; *length = 2; break;
-        default:                           *terminator = "\r";   *length = 1; break;
+
+        case GUAC_SERIAL_LINE_ENDING_LF:
+            *terminator = "\n";
+            *length = 1;
+            break;
+
+        case GUAC_SERIAL_LINE_ENDING_CRLF:
+            *terminator = "\r\n";
+            *length = 2;
+            break;
+
+        default:
+            *terminator = "\r";
+            *length = 1;
+            break;
+
     }
 }

diff --git a/src/protocols/serial/settings.c b/src/protocols/serial/settings.c
index 8a6aa9f1..88eb6e2d 100644
--- a/src/protocols/serial/settings.c
+++ b/src/protocols/serial/settings.c
@@ -26,13 +26,11 @@
 #include "terminal/terminal.h"

 #include <guacamole/mem.h>
-#include <guacamole/string.h>
 #include <guacamole/user.h>

 #include <sys/types.h>
 #include <termios.h>
 #include <errno.h>
-#include <limits.h>
 #include <stdlib.h>
 #include <string.h>

@@ -51,7 +49,6 @@ const char* GUAC_SERIAL_CLIENT_ARGS[] = {
     "flow-control",
     "break-duration",
     "paste-delay",
-    "allowed-devices",
     "read-only",
     GUAC_SERIAL_ARGV_FONT_NAME,
     GUAC_SERIAL_ARGV_FONT_SIZE,
@@ -88,106 +85,109 @@ enum SERIAL_ARGS_IDX {

     /**
      * The transport used to reach the serial line ("local" or "network").
+     * Defaults to "local".
      */
     IDX_SERIAL_TYPE,

     /**
-     * The network protocol used when the transport type is "network" ("raw" or
-     * "rfc2217").
+     * The network protocol used when the transport type is "network" ("raw",
+     * "rfc2217", or "telnet"). Defaults to "raw".
      */
     IDX_NETWORK_PROTOCOL,

     /**
      * The absolute path of the local serial device to open (e.g.
-     * "/dev/ttyUSB0"). Required for "local" connections.
+     * "/dev/ttyUSB0"). Required for "local" connections. There is no default.
      */
     IDX_DEVICE,

     /**
      * The hostname of the network serial server to connect to. Required for
-     * "network" connections.
+     * "network" connections. There is no default.
      */
     IDX_HOSTNAME,

     /**
      * The port of the network serial server to connect to. Required for
-     * "network" connections.
+     * "network" connections. There is no default.
      */
     IDX_PORT,

     /**
-     * The number of seconds to wait for the remote server to respond. Optional.
+     * The number of seconds to wait for the remote server to respond. Defaults
+     * to GUAC_SERIAL_DEFAULT_TIMEOUT (10 seconds).
      */
     IDX_TIMEOUT,

     /**
-     * The serial line speed, in bits per second (e.g. 9600, 115200).
+     * The serial line speed, in bits per second (e.g. 9600, 115200). Defaults
+     * to 9600.
      */
     IDX_BAUD_RATE,

     /**
-     * The number of data bits per character (5, 6, 7, or 8).
+     * The number of data bits per character (5, 6, 7, or 8). Defaults to 8.
      */
     IDX_DATA_BITS,

     /**
-     * The number of stop bits per character (1 or 2).
+     * The number of stop bits per character (1 or 2). Defaults to 1.
      */
     IDX_STOP_BITS,

     /**
-     * The parity scheme ("none", "odd", "even", "mark", or "space").
+     * The parity scheme ("none", "odd", "even", "mark", or "space"). Defaults
+     * to "none".
      */
     IDX_PARITY,

     /**
-     * The flow control scheme ("none", "rts-cts", or "xon-xoff").
+     * The flow control scheme ("none", "rts-cts", or "xon-xoff"). Defaults to
+     * "none".
      */
     IDX_FLOW_CONTROL,

     /**
      * The duration, in milliseconds, that the transmit line is held low when
-     * sending a serial break.
+     * sending a serial break. Defaults to GUAC_SERIAL_DEFAULT_BREAK_DURATION
+     * (500 milliseconds).
      */
     IDX_BREAK_DURATION,

     /**
      * The delay, in milliseconds, inserted between each byte written to the
-     * serial line. Zero disables pacing.
+     * serial line. Zero disables pacing. Defaults to 0.
      */
     IDX_PASTE_DELAY,

-    /**
-     * An optional ":"-separated allowlist of local device paths or path
-     * prefixes. If set, a "local" connection whose canonicalized device does
-     * not equal or fall under one of the entries is rejected.
-     */
-    IDX_ALLOWED_DEVICES,
-
     /**
      * "true" if this connection should be read-only (user input should be
-     * dropped), "false" or blank otherwise.
+     * dropped), "false" or blank otherwise. Defaults to false.
      */
     IDX_READ_ONLY,

     /**
-     * The name of the font to use within the terminal.
+     * The name of the font to use within the terminal. Defaults to
+     * GUAC_TERMINAL_DEFAULT_FONT_NAME ("monospace").
      */
     IDX_FONT_NAME,

     /**
-     * The size of the font to use within the terminal, in points.
+     * The size of the font to use within the terminal, in points. Defaults to
+     * GUAC_TERMINAL_DEFAULT_FONT_SIZE (12).
      */
     IDX_FONT_SIZE,

     /**
      * The color scheme to use, as a series of semicolon-separated color-value
-     * pairs, or one of the special color scheme names.
+     * pairs, or one of the special color scheme names. Defaults to
+     * GUAC_TERMINAL_DEFAULT_COLOR_SCHEME (the default "gray-black" scheme).
      */
     IDX_COLOR_SCHEME,

     /**
-     * The maximum size of the scrollback buffer in rows.
+     * The maximum size of the scrollback buffer in rows. Defaults to
+     * GUAC_TERMINAL_DEFAULT_MAX_SCROLLBACK (1000).
      */
     IDX_SCROLLBACK,

@@ -204,35 +204,38 @@ enum SERIAL_ARGS_IDX {
     IDX_TERMINAL_TYPE,

     /**
-     * The maximum number of bytes to allow within the clipboard.
+     * The maximum number of bytes to allow within the clipboard. If unspecified
+     * or below GUAC_COMMON_CLIPBOARD_MIN_LENGTH,
+     * GUAC_COMMON_CLIPBOARD_MIN_LENGTH is used.
      */
     IDX_CLIPBOARD_BUFFER_SIZE,

     /**
-     * Whether outbound clipboard access should be blocked.
+     * Whether outbound clipboard access should be blocked. Defaults to false.
      */
     IDX_DISABLE_COPY,

     /**
-     * Whether inbound clipboard access should be blocked.
+     * Whether inbound clipboard access should be blocked. Defaults to false.
      */
     IDX_DISABLE_PASTE,

     /**
      * The full absolute path to the directory in which typescripts should be
-     * written.
+     * written. There is no default; if unset, no typescript is written.
      */
     IDX_TYPESCRIPT_PATH,

     /**
      * The name that should be given to typescripts which are written in the
-     * given path.
+     * given path. Defaults to GUAC_SERIAL_DEFAULT_TYPESCRIPT_NAME
+     * ("typescript").
      */
     IDX_TYPESCRIPT_NAME,

     /**
      * Whether the specified typescript path should automatically be created if
-     * it does not yet exist.
+     * it does not yet exist. Defaults to false.
      */
     IDX_CREATE_TYPESCRIPT_PATH,

@@ -243,43 +246,45 @@ enum SERIAL_ARGS_IDX {
     IDX_TYPESCRIPT_WRITE_EXISTING,

     /**
-     * The full absolute path to the directory in which screen recordings
-     * should be written.
+     * The full absolute path to the directory in which screen recordings should
+     * be written. There is no default; if unset, no recording is written.
      */
     IDX_RECORDING_PATH,

     /**
      * The name that should be given to screen recordings which are written in
-     * the given path.
+     * the given path. Defaults to GUAC_SERIAL_DEFAULT_RECORDING_NAME
+     * ("recording").
      */
     IDX_RECORDING_NAME,

     /**
      * Whether output which is broadcast to each connected client should NOT be
-     * included in the session recording.
+     * included in the session recording. Defaults to false.
      */
     IDX_RECORDING_EXCLUDE_OUTPUT,

     /**
      * Whether changes to mouse state should NOT be included in the session
-     * recording.
+     * recording. Defaults to false.
      */
     IDX_RECORDING_EXCLUDE_MOUSE,

     /**
      * Whether keys pressed and released should be included in the session
-     * recording.
+     * recording. Defaults to false.
      */
     IDX_RECORDING_INCLUDE_KEYS,

     /**
      * Whether clipboard data should be included in the session recording.
+     * Defaults to false.
      */
     IDX_RECORDING_INCLUDE_CLIPBOARD,

     /**
      * Whether the specified screen recording path should automatically be
-     * created if it does not yet exist.
+     * created if it does not yet exist. Defaults to false.
      */
     IDX_CREATE_RECORDING_PATH,

@@ -321,7 +326,8 @@ enum SERIAL_ARGS_IDX {

     /**
      * The number of seconds to wait for an inbound connection in reverse mode
-     * before failing. Zero or negative means wait indefinitely.
+     * before failing. Zero or negative means wait indefinitely. Defaults to
+     * GUAC_SERIAL_DEFAULT_LISTEN_TIMEOUT (60 seconds).
      */
     IDX_LISTEN_TIMEOUT,

@@ -337,17 +343,40 @@ enum SERIAL_ARGS_IDX {
 speed_t guac_serial_baud_to_speed(int baud_rate) {

     switch (baud_rate) {
-        case 300:    return B300;
-        case 1200:   return B1200;
-        case 2400:   return B2400;
-        case 4800:   return B4800;
-        case 9600:   return B9600;
-        case 19200:  return B19200;
-        case 38400:  return B38400;
-        case 57600:  return B57600;
-        case 115200: return B115200;
-        case 230400: return B230400;
-        default:     return (speed_t) -1;
+
+        case 300:
+            return B300;
+
+        case 1200:
+            return B1200;
+
+        case 2400:
+            return B2400;
+
+        case 4800:
+            return B4800;
+
+        case 9600:
+            return B9600;
+
+        case 19200:
+            return B19200;
+
+        case 38400:
+            return B38400;
+
+        case 57600:
+            return B57600;
+
+        case 115200:
+            return B115200;
+
+        case 230400:
+            return B230400;
+
+        default:
+            return (speed_t) -1;
+
     }

 }
@@ -366,9 +395,19 @@ speed_t guac_serial_baud_to_speed(int baud_rate) {
  *     Zero if the string was successfully parsed, non-zero otherwise.
  */
 static int guac_serial_parse_type(const char* str, guac_serial_type* type) {
-    if (strcmp(str, "local") == 0)   { *type = GUAC_SERIAL_TYPE_LOCAL;   return 0; }
-    if (strcmp(str, "network") == 0) { *type = GUAC_SERIAL_TYPE_NETWORK; return 0; }
+
+    if (strcmp(str, "local") == 0) {
+        *type = GUAC_SERIAL_TYPE_LOCAL;
+        return 0;
+    }
+
+    if (strcmp(str, "network") == 0) {
+        *type = GUAC_SERIAL_TYPE_NETWORK;
+        return 0;
+    }
+
     return 1;
+
 }

 /**
@@ -376,7 +415,7 @@ static int guac_serial_parse_type(const char* str, guac_serial_type* type) {
  * guac_serial_network_protocol value.
  *
  * @param str
- *     The network protocol string ("raw" or "rfc2217").
+ *     The network protocol string ("raw", "rfc2217", or "telnet").
  *
  * @param protocol
  *     A pointer to the value that should receive the parsed result.
@@ -386,10 +425,24 @@ static int guac_serial_parse_type(const char* str, guac_serial_type* type) {
  */
 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; }
+
+    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;
+
 }

 /**
@@ -404,13 +457,36 @@ static int guac_serial_parse_network_protocol(const char* str,
  * @return
  *     Zero if the string was successfully parsed, non-zero otherwise.
  */
-static int guac_serial_parse_parity(const char* str, guac_serial_parity* parity) {
-    if (strcmp(str, "none") == 0)  { *parity = GUAC_SERIAL_PARITY_NONE;  return 0; }
-    if (strcmp(str, "odd") == 0)   { *parity = GUAC_SERIAL_PARITY_ODD;   return 0; }
-    if (strcmp(str, "even") == 0)  { *parity = GUAC_SERIAL_PARITY_EVEN;  return 0; }
-    if (strcmp(str, "mark") == 0)  { *parity = GUAC_SERIAL_PARITY_MARK;  return 0; }
-    if (strcmp(str, "space") == 0) { *parity = GUAC_SERIAL_PARITY_SPACE; return 0; }
+static int guac_serial_parse_parity(const char* str,
+        guac_serial_parity* parity) {
+
+    if (strcmp(str, "none") == 0) {
+        *parity = GUAC_SERIAL_PARITY_NONE;
+        return 0;
+    }
+
+    if (strcmp(str, "odd") == 0) {
+        *parity = GUAC_SERIAL_PARITY_ODD;
+        return 0;
+    }
+
+    if (strcmp(str, "even") == 0) {
+        *parity = GUAC_SERIAL_PARITY_EVEN;
+        return 0;
+    }
+
+    if (strcmp(str, "mark") == 0) {
+        *parity = GUAC_SERIAL_PARITY_MARK;
+        return 0;
+    }
+
+    if (strcmp(str, "space") == 0) {
+        *parity = GUAC_SERIAL_PARITY_SPACE;
+        return 0;
+    }
+
     return 1;
+
 }

 /**
@@ -425,11 +501,26 @@ static int guac_serial_parse_parity(const char* str, guac_serial_parity* parity)
  * @return
  *     Zero if the string was successfully parsed, non-zero otherwise.
  */
-static int guac_serial_parse_flow(const char* str, guac_serial_flow_control* flow) {
-    if (strcmp(str, "none") == 0)     { *flow = GUAC_SERIAL_FLOW_NONE;     return 0; }
-    if (strcmp(str, "rts-cts") == 0)  { *flow = GUAC_SERIAL_FLOW_RTS_CTS;  return 0; }
-    if (strcmp(str, "xon-xoff") == 0) { *flow = GUAC_SERIAL_FLOW_XON_XOFF; return 0; }
+static int guac_serial_parse_flow(const char* str,
+        guac_serial_flow_control* flow) {
+
+    if (strcmp(str, "none") == 0) {
+        *flow = GUAC_SERIAL_FLOW_NONE;
+        return 0;
+    }
+
+    if (strcmp(str, "rts-cts") == 0) {
+        *flow = GUAC_SERIAL_FLOW_RTS_CTS;
+        return 0;
+    }
+
+    if (strcmp(str, "xon-xoff") == 0) {
+        *flow = GUAC_SERIAL_FLOW_XON_XOFF;
+        return 0;
+    }
+
     return 1;
+
 }

 /**
@@ -446,54 +537,23 @@ static int guac_serial_parse_flow(const char* str, guac_serial_flow_control* flo
  */
 static int guac_serial_parse_line_ending(const char* str,
         guac_serial_line_ending* line_ending) {
-    if (strcmp(str, "cr") == 0)   { *line_ending = GUAC_SERIAL_LINE_ENDING_CR;   return 0; }
-    if (strcmp(str, "lf") == 0)   { *line_ending = GUAC_SERIAL_LINE_ENDING_LF;   return 0; }
-    if (strcmp(str, "crlf") == 0) { *line_ending = GUAC_SERIAL_LINE_ENDING_CRLF; return 0; }
-    return 1;
-}
-
-bool guac_serial_device_permitted(const char* device, const char* allowed) {
-
-    char real_device[PATH_MAX];
-    if (realpath(device, real_device) == NULL)
-        return false;

-    /* Duplicate the list so it may be tokenized in place */
-    char* list = guac_strdup(allowed);
-    char* saveptr = NULL;
-    bool permitted = false;
-
-    for (char* token = strtok_r(list, ":", &saveptr); token != NULL;
-            token = strtok_r(NULL, ":", &saveptr)) {
-
-        /* Skip empty entries */
-        if (token[0] == '\0')
-            continue;
-
-        /* Canonicalize the allowlist entry; skip entries that do not resolve */
-        char real_entry[PATH_MAX];
-        if (realpath(token, real_entry) == NULL)
-            continue;
-
-        size_t entry_len = strlen(real_entry);
-
-        /* Permit an exact match */
-        if (strcmp(real_device, real_entry) == 0) {
-            permitted = true;
-            break;
-        }
+    if (strcmp(str, "cr") == 0) {
+        *line_ending = GUAC_SERIAL_LINE_ENDING_CR;
+        return 0;
+    }

-        /* Permit a device that falls beneath a permitted prefix */
-        if (strncmp(real_device, real_entry, entry_len) == 0
-                && real_device[entry_len] == '/') {
-            permitted = true;
-            break;
-        }
+    if (strcmp(str, "lf") == 0) {
+        *line_ending = GUAC_SERIAL_LINE_ENDING_LF;
+        return 0;
+    }

+    if (strcmp(str, "crlf") == 0) {
+        *line_ending = GUAC_SERIAL_LINE_ENDING_CRLF;
+        return 0;
     }

-    guac_mem_free(list);
-    return permitted;
+    return 1;

 }

@@ -522,99 +582,109 @@ guac_serial_settings* guac_serial_parse_args(guac_user* user,
     }
     guac_mem_free(type_str);

-    /* Parse network protocol */
-    char* proto_str = guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS,
-            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\", \"rfc2217\", or \"telnet\".", proto_str);
-        guac_mem_free(proto_str);
-        guac_mem_free(settings);
-        return NULL;
-    }
-    guac_mem_free(proto_str);
-
-    /* Parse local device path */
-    settings->device =
-        guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
-                IDX_DEVICE, NULL);
-
-    /* Parse network connection information */
-    settings->hostname =
-        guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
-                IDX_HOSTNAME, NULL);
-
-    settings->port =
-        guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
-                IDX_PORT, NULL);
-
-    settings->timeout =
-        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 device path, the only transport-specific parameter of
+     * local connections */
+    if (settings->type == GUAC_SERIAL_TYPE_LOCAL) {

-    /* 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");
+        settings->device =
+            guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+                    IDX_DEVICE, NULL);

-    /* 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') {
             guac_user_log(user, GUAC_LOG_ERROR, "A \"device\" is required for "
                     "local serial connections.");
             guac_serial_settings_free(settings);
             return NULL;
         }
+
+        /* Reverse mode is meaningless for local connections. The flag is
+         * parsed here only to warn the user that it is being ignored. */
+        if (guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+                    IDX_REVERSE_CONNECT, false))
+            guac_user_log(user, GUAC_LOG_WARNING, "reverse-connect has no "
+                    "effect on local serial connections; ignoring.");
+
     }
+
+    /* Parse network connection information */
     else {
+
+        /* Parse network protocol */
+        char* proto_str = guac_user_parse_args_string(user,
+                GUAC_SERIAL_CLIENT_ARGS, 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\", \"rfc2217\", or \"telnet\".",
+                    proto_str);
+            guac_mem_free(proto_str);
+            guac_serial_settings_free(settings);
+            return NULL;
+        }
+        guac_mem_free(proto_str);
+
+#ifndef ENABLE_SERIAL_RFC2217
+        /* Reject RFC2217/telnet if libtelnet support was not compiled in */
+        if (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;
+        }
+#endif
+
+        settings->hostname =
+            guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+                    IDX_HOSTNAME, NULL);
+
+        settings->port =
+            guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
+                    IDX_PORT, NULL);
+
+        settings->timeout =
+            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");
+
         /* 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.");
+                && (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);
             return NULL;
         }
+
         if (settings->port == NULL || settings->port[0] == '\0') {
             guac_user_log(user, GUAC_LOG_ERROR, "A \"port\" is required for "
                     "network serial connections.");
             guac_serial_settings_free(settings);
             return NULL;
         }
-    }

-#ifndef ENABLE_SERIAL_RFC2217
-    /* 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
-                || 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;
     }
-#endif

     /* Parse serial line speed */
     settings->baud_rate =
@@ -715,25 +785,6 @@ guac_serial_settings* guac_serial_parse_args(guac_user* user,
         guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
                 IDX_HANGUP_ON_CLOSE, false);

-    /* Retain the device allowlist so it can be re-checked on every (re)open */
-    settings->allowed_devices =
-        guac_user_parse_args_string(user, GUAC_SERIAL_CLIENT_ARGS, argv,
-                IDX_ALLOWED_DEVICES, NULL);
-
-    /* Enforce local device allowlist at parse time, if configured */
-    if (settings->type == GUAC_SERIAL_TYPE_LOCAL
-            && settings->allowed_devices != NULL
-            && settings->allowed_devices[0] != '\0') {
-        if (!guac_serial_device_permitted(settings->device,
-                settings->allowed_devices)) {
-            guac_user_log(user, GUAC_LOG_ERROR, "Serial device \"%s\" is not "
-                    "permitted by the configured allowed-devices list, or "
-                    "could not be resolved.", settings->device);
-            guac_serial_settings_free(settings);
-            return NULL;
-        }
-    }
-
     /* Read-only mode */
     settings->read_only =
         guac_user_parse_args_boolean(user, GUAC_SERIAL_CLIENT_ARGS, argv,
@@ -874,7 +925,6 @@ void guac_serial_settings_free(guac_serial_settings* settings) {

     /* Free transport connection information */
     guac_mem_free(settings->device);
-    guac_mem_free(settings->allowed_devices);
     guac_mem_free(settings->hostname);
     guac_mem_free(settings->port);
     guac_mem_free(settings->bind_address);
diff --git a/src/protocols/serial/settings.h b/src/protocols/serial/settings.h
index 65c5ba92..7ae6e8cc 100644
--- a/src/protocols/serial/settings.h
+++ b/src/protocols/serial/settings.h
@@ -26,12 +26,6 @@
 #include <sys/types.h>
 #include <stdbool.h>

-/**
- * The port to connect to when initiating any network serial connection, if no
- * other port is specified.
- */
-#define GUAC_SERIAL_DEFAULT_PORT "2000"
-
 /**
  * The protocol label included in the process title (the first argument passed
  * to guac_process_title_set_endpoint()), as seen in `ps`/`top`.
@@ -84,7 +78,8 @@ typedef enum guac_serial_type {
     GUAC_SERIAL_TYPE_LOCAL,

     /**
-     * A remote serial line reached over the network (raw TCP or RFC2217).
+     * A remote serial line reached over the network (raw TCP, RFC2217, or
+     * Telnet).
      */
     GUAC_SERIAL_TYPE_NETWORK

@@ -123,20 +118,58 @@ typedef enum guac_serial_network_protocol {
  * The parity scheme applied to the serial line.
  */
 typedef enum guac_serial_parity {
+
+    /**
+     * No parity bit is sent or checked.
+     */
     GUAC_SERIAL_PARITY_NONE,
+
+    /**
+     * A parity bit is sent such that the total number of set bits in each
+     * character, including the parity bit, is odd.
+     */
     GUAC_SERIAL_PARITY_ODD,
+
+    /**
+     * A parity bit is sent such that the total number of set bits in each
+     * character, including the parity bit, is even.
+     */
     GUAC_SERIAL_PARITY_EVEN,
+
+    /**
+     * A parity bit is sent which is always set (1).
+     */
     GUAC_SERIAL_PARITY_MARK,
+
+    /**
+     * A parity bit is sent which is always cleared (0).
+     */
     GUAC_SERIAL_PARITY_SPACE
+
 } guac_serial_parity;

 /**
  * The flow control scheme applied to the serial line.
  */
 typedef enum guac_serial_flow_control {
+
+    /**
+     * No flow control is used.
+     */
     GUAC_SERIAL_FLOW_NONE,
+
+    /**
+     * Hardware flow control, using the RTS (Request To Send) and CTS (Clear To
+     * Send) control lines.
+     */
     GUAC_SERIAL_FLOW_RTS_CTS,
+
+    /**
+     * Software flow control, using in-band XON (DC1, 0x11) and XOFF (DC3,
+     * 0x13) characters.
+     */
     GUAC_SERIAL_FLOW_XON_XOFF
+
 } guac_serial_flow_control;

 /**
@@ -187,14 +220,6 @@ typedef struct guac_serial_settings {
      */
     char* device;

-    /**
-     * An optional ":"-separated allowlist of permitted local device paths or
-     * path prefixes, or NULL if no allowlist was configured. Retained so the
-     * device can be re-validated on every (re)open, guarding against a symlink
-     * being repointed at a disallowed device between reconnects.
-     */
-    char* allowed_devices;
-
     /**
      * The hostname of the network serial server to connect to when type is
      * GUAC_SERIAL_TYPE_NETWORK. NULL if not a network connection.
@@ -202,14 +227,17 @@ typedef struct guac_serial_settings {
     char* hostname;

     /**
-     * The port of the network serial server to connect to when type is
-     * GUAC_SERIAL_TYPE_NETWORK.
+     * The port of the network serial server to connect to (or, in reverse
+     * mode, to listen on) when type is GUAC_SERIAL_TYPE_NETWORK. NULL if not a
+     * network connection.
      */
     char* port;

     /**
      * Whether guacd should listen for an inbound connection from the device
-     * (reverse mode) instead of dialing out. Only meaningful for network type.
+     * (reverse mode) instead of dialing out. Only takes effect for network
+     * connections; for local connections the parameter is read solely to
+     * warn that it is ignored, and this value is always false.
      */
     bool reverse_connect;

@@ -221,12 +249,13 @@ typedef struct guac_serial_settings {

     /**
      * Local address to bind when listening in reverse mode. NULL/empty =>
-     * 127.0.0.1.
+     * 127.0.0.1. Always NULL for local connections, which do not parse it.
      */
     char* bind_address;

     /**
-     * The number of seconds to wait for a connection before timing out.
+     * The number of seconds to wait for a network connection before timing
+     * out. Unused (zero) for local connections.
      */
     int timeout;

@@ -482,28 +511,6 @@ guac_serial_settings* guac_serial_parse_args(guac_user* user,
  */
 speed_t guac_serial_baud_to_speed(int baud_rate);

-/**
- * Returns whether the given local device is permitted by the given ":"-
- * separated allowlist of device paths or path prefixes. Both the device and
- * each allowlist entry are canonicalized with realpath() before comparison; a
- * device is permitted if its canonical path equals a canonicalized entry or
- * falls beneath one of the canonicalized entries. If the device cannot be
- * resolved (e.g. it does not currently exist), it is not permitted.
- *
- * This performs no logging so that it may be reused both at parse time and on
- * every (re)open.
- *
- * @param device
- *     The local device path to test.
- *
- * @param allowed
- *     The ":"-separated allowlist of permitted device paths or prefixes.
- *
- * @return
- *     true if the device is permitted, false otherwise.
- */
-bool guac_serial_device_permitted(const char* device, const char* allowed);
-
 /**
  * Frees the given guac_serial_settings object, having been previously
  * allocated via guac_serial_parse_args().
diff --git a/src/protocols/serial/stream.h b/src/protocols/serial/stream.h
index e488aecb..6b668ce0 100644
--- a/src/protocols/serial/stream.h
+++ b/src/protocols/serial/stream.h
@@ -88,16 +88,16 @@ typedef struct guac_serial_stream {

     /**
      * The device file descriptor (local) or socket file descriptor (tcp,
-     * rfc2217), or -1 if not open.
+     * rfc2217, telnet), or -1 if not open.
      */
     int fd;

     /**
      * The libtelnet session (telnet_t*) used for IAC framing when the backend
-     * is GUAC_SERIAL_BACKEND_RFC2217, or NULL otherwise. Declared as an opaque
-     * pointer so this header does not depend on libtelnet, which is required
-     * only by the RFC2217 backend (see rfc2217.c). Only the RFC2217 backend
-     * dereferences this value.
+     * is GUAC_SERIAL_BACKEND_RFC2217 or GUAC_SERIAL_BACKEND_TELNET, or NULL
+     * otherwise. Declared as an opaque pointer so this header does not depend
+     * on libtelnet, which is required only by those two backends (see
+     * rfc2217.c). Only the RFC2217 and Telnet backends dereference this value.
      */
     void* telnet;

@@ -153,9 +153,9 @@ guac_serial_stream* guac_serial_stream_alloc(guac_client* client,
 /**
  * (Re)opens the transport for the given serial stream, closing any currently-
  * open file descriptor and libtelnet session first, then dispatching to the
- * local, TCP, or RFC2217 backend as appropriate. The stream's fd is swapped
- * under its write lock so that a concurrent writer never touches a stale
- * descriptor. On failure, the stream's open_status is set and the fd is left
+ * local, TCP, RFC2217, or Telnet backend as appropriate. The stream's fd is
+ * swapped under its write lock so that a concurrent writer never touches a
+ * stale descriptor. On failure, the stream's open_status is set and the fd is left
  * at -1; the client is NOT aborted, so the caller may retry.
  *
  * @param stream
@@ -197,9 +197,10 @@ int guac_serial_stream_write(guac_serial_stream* stream, const char* buffer,

 /**
  * Sends a serial break on the serial stream. For local and RFC2217 transports,
- * the transmit line is held low for the configured break duration. Raw TCP
- * transports do not support break signalling; a warning is logged and the call
- * is a no-op.
+ * the transmit line is held low for the configured break duration. For the
+ * Telnet transport, a Telnet NVT BRK command (IAC BRK) is sent, leaving it to
+ * the remote server to generate the break. Raw TCP transports do not support
+ * break signalling; a warning is logged and the call is a no-op.
  *
  * @param stream
  *     The serial stream on which to send the break.
@@ -213,8 +214,8 @@ int guac_serial_stream_send_break(guac_serial_stream* stream);
  * Asserts or de-asserts a modem control line (DTR or RTS) on the serial stream.
  * For local transports the line is toggled via TIOCMBIS/TIOCMBIC; for RFC2217
  * transports the corresponding COM-PORT-OPTION SET-CONTROL subnegotiation is
- * sent. Raw TCP transports do not support control-line signalling; a warning
- * is logged and the call is a no-op.
+ * sent. The raw TCP and Telnet transports do not support control-line
+ * signalling; a warning is logged and the call is a no-op.
  *
  * @param stream
  *     The serial stream on which to toggle the control line.
diff --git a/src/protocols/serial/tcp.c b/src/protocols/serial/tcp.c
index 6b73f973..104a93cf 100644
--- a/src/protocols/serial/tcp.c
+++ b/src/protocols/serial/tcp.c
@@ -222,23 +222,34 @@ int guac_serial_net_open_fd(guac_serial_stream* stream, guac_client* client,

             /* 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;
+            switch (err) {
+
+                case 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;
+                    break;
+
+                case 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;
+                    break;
+
+                default:
+                    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;
+                    break;
+
             }

             return -1;
diff --git a/src/protocols/serial/tcp.h b/src/protocols/serial/tcp.h
index c1e182a9..3a8120fb 100644
--- a/src/protocols/serial/tcp.h
+++ b/src/protocols/serial/tcp.h
@@ -28,7 +28,9 @@
 /**
  * Opens a raw TCP connection to the network serial server described by the
  * given settings (e.g. ser2net operating in raw mode) and stores the resulting
- * socket in the given stream. On failure, the client is aborted.
+ * socket in the given stream. On failure, the
+ * stream's open_status is set and non-zero is returned; whether to retry is
+ * left to the caller.
  *
  * @param stream
  *     The serial stream to populate with the opened socket.
@@ -50,8 +52,8 @@ int guac_serial_tcp_open(guac_serial_stream* stream, guac_client* client,
  * (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.
+ * This is shared by the raw TCP, RFC2217, and Telnet backends so that reverse
+ * mode need only be implemented once.
  *
  * @param stream
  *     The serial stream whose open_status is set on failure.