From 57d287ff0513e51c7d6074dba7e3766f8b36a6e1 Mon Sep 17 00:00:00 2001 From: Bradley Bennett Date: Thu, 28 May 2026 22:32:57 -0400 Subject: [PATCH] GUACAMOLE-2283: Runtime observability: add guacd process & thread naming. --- src/guacd/connection.c | 14 +- src/guacd/daemon.c | 4 +- src/guacd/proc.c | 14 + src/libguac/Makefile.am | 3 +- src/libguac/client.c | 5 + src/libguac/display-render-thread.c | 5 + src/libguac/display-worker.c | 5 + src/libguac/guacamole/proctitle.h | 167 ++++++++ src/libguac/proctitle.c | 372 ++++++++++++++++++ src/libguac/socket.c | 5 + src/libguac/tcp.c | 5 + src/libguac/user-handshake.c | 5 + src/protocols/kubernetes/kubernetes.c | 33 ++ src/protocols/kubernetes/settings.h | 6 + .../rdp/channels/audio-input/audio-buffer.c | 5 + src/protocols/rdp/print-job.c | 5 + src/protocols/rdp/rdp.c | 11 + src/protocols/rdp/settings.h | 6 + src/protocols/ssh/settings.h | 6 + src/protocols/ssh/ssh.c | 14 + src/protocols/telnet/settings.h | 6 + src/protocols/telnet/telnet.c | 14 + src/protocols/vnc/settings.h | 6 + src/protocols/vnc/vnc.c | 15 + src/terminal/terminal.c | 5 + 25 files changed, 733 insertions(+), 3 deletions(-) create mode 100644 src/libguac/guacamole/proctitle.h create mode 100644 src/libguac/proctitle.c diff --git a/src/guacd/connection.c b/src/guacd/connection.c index c07d6643bc..7f5e7e864f 100644 --- a/src/guacd/connection.c +++ b/src/guacd/connection.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -106,6 +107,10 @@ static int __write_all(int fd, char* buffer, int length) { */ static void* guacd_connection_write_thread(void* data) { + /* Thread name conn-write: forwards data from the connected user to the + * connection's child process. */ + guac_thread_name_set("conn-write"); + guacd_connection_io_thread_params* params = (guacd_connection_io_thread_params*) data; char buffer[8192]; @@ -132,6 +137,10 @@ static void* guacd_connection_write_thread(void* data) { void* guacd_connection_io_thread(void* data) { + /* Thread name conn-read: forwards data from the connection's child + * process back to the connected user. */ + guac_thread_name_set("conn-read"); + guacd_connection_io_thread_params* params = (guacd_connection_io_thread_params*) data; char buffer[8192]; @@ -372,6 +381,10 @@ static int guacd_route_connection(guacd_proc_map* map, guac_socket* socket) { void* guacd_connection_thread(void* data) { + /* Thread name conn-route: performs the protocol handshake for a new + * client connection and routes it to a connection process. */ + guac_thread_name_set("conn-route"); + guacd_connection_thread_params* params = (guacd_connection_thread_params*) data; guacd_proc_map* map = params->map; @@ -409,4 +422,3 @@ void* guacd_connection_thread(void* data) { return NULL; } - diff --git a/src/guacd/daemon.c b/src/guacd/daemon.c index b374eb9b17..61b83e2102 100644 --- a/src/guacd/daemon.c +++ b/src/guacd/daemon.c @@ -27,6 +27,7 @@ #include "proc-map.h" #include +#include #ifdef ENABLE_SSL #include @@ -296,6 +297,8 @@ static void stop_process_callback(guacd_proc* proc, void* data) { int main(int argc, char* argv[]) { + guac_process_title_init(argc, argv); + /* Server */ int socket_fd; struct addrinfo* addresses; @@ -614,4 +617,3 @@ int main(int argc, char* argv[]) { return 0; } - diff --git a/src/guacd/proc.c b/src/guacd/proc.c index 4040a3be23..ec717d78a3 100644 --- a/src/guacd/proc.c +++ b/src/guacd/proc.c @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -80,6 +81,10 @@ typedef struct guacd_user_thread_params { */ static void* guacd_user_thread(void* data) { + /* Thread name user-conn: manages a single user's connection lifecycle + * from handshake through disconnect. */ + guac_thread_name_set("user-conn"); + guacd_user_thread_params* params = (guacd_user_thread_params*) data; guacd_proc* proc = params->proc; guac_client* client = proc->client; @@ -213,6 +218,10 @@ typedef struct guacd_client_free { */ static void* guacd_client_free_thread(void* data) { + /* Thread name client-free: frees a guac_client in the background, + * bounded by a timeout in case the free handler hangs. */ + guac_thread_name_set("client-free"); + guacd_client_free* free_operation = (guacd_client_free*) data; /* Attempt to free client (this may never return if the client is @@ -326,6 +335,11 @@ static void guacd_exec_proc(guacd_proc* proc, const char* protocol) { int result = 1; + /* Label the new child process. guac_process_title_set() also writes the + * main thread's comm, which is the current thread here, so a separate + * guac_thread_name_set() call would just duplicate the work. */ + guac_process_title_set(protocol); + /* Set process group ID to match PID */ if (setpgid(0, 0)) { guacd_log(GUAC_LOG_ERROR, "Cannot set PGID for connection process: %s", diff --git a/src/libguac/Makefile.am b/src/libguac/Makefile.am index 6e2e3f14d9..2898444546 100644 --- a/src/libguac/Makefile.am +++ b/src/libguac/Makefile.am @@ -74,6 +74,7 @@ libguacinc_HEADERS = \ guacamole/plugin.h \ guacamole/pool.h \ guacamole/pool-types.h \ + guacamole/proctitle.h \ guacamole/protocol.h \ guacamole/protocol-constants.h \ guacamole/protocol-types.h \ @@ -155,6 +156,7 @@ libguac_la_SOURCES = \ palette.c \ parser.c \ pool.c \ + proctitle.c \ protocol.c \ raw_encoder.c \ recording.c \ @@ -209,4 +211,3 @@ libguac_la_LDFLAGS = \ @VORBIS_LIBS@ \ @WEBP_LIBS@ \ @WINSOCK_LIBS@ - diff --git a/src/libguac/client.c b/src/libguac/client.c index 5926e9ca6b..e9635c2acf 100644 --- a/src/libguac/client.c +++ b/src/libguac/client.c @@ -28,6 +28,7 @@ #include "guacamole/layer.h" #include "guacamole/plugin.h" #include "guacamole/pool.h" +#include "guacamole/proctitle.h" #include "guacamole/protocol.h" #include "guacamole/rwlock.h" #include "guacamole/socket.h" @@ -240,6 +241,10 @@ static void guac_client_promote_pending_users(guac_client* client) { */ static void* guac_client_pending_users_thread(void* data) { + /* Thread name user-pending: periodically promotes pending users into + * the active connection. */ + guac_thread_name_set("user-pending"); + guac_client* client = (guac_client*) data; while (client->state == GUAC_CLIENT_RUNNING) { diff --git a/src/libguac/display-render-thread.c b/src/libguac/display-render-thread.c index 345ffb44c5..933442ada4 100644 --- a/src/libguac/display-render-thread.c +++ b/src/libguac/display-render-thread.c @@ -23,6 +23,7 @@ #include "guacamole/display.h" #include "guacamole/flag.h" #include "guacamole/mem.h" +#include "guacamole/proctitle.h" #include "guacamole/timestamp.h" /** @@ -62,6 +63,10 @@ */ static void* guac_display_render_loop(void* data) { + /* Thread name display-render: drives the display render loop, flushing + * completed frames to the client. */ + guac_thread_name_set("display-render"); + guac_display_render_thread* render_thread = (guac_display_render_thread*) data; guac_display* display = render_thread->display; guac_client* client = display->client; diff --git a/src/libguac/display-worker.c b/src/libguac/display-worker.c index eb251263c6..7f500cf36d 100644 --- a/src/libguac/display-worker.c +++ b/src/libguac/display-worker.c @@ -23,6 +23,7 @@ #include "guacamole/display.h" #include "guacamole/fifo.h" #include "guacamole/layer.h" +#include "guacamole/proctitle.h" #include "guacamole/protocol-types.h" #include "guacamole/protocol.h" #include "guacamole/rect.h" @@ -293,6 +294,10 @@ static int LFR_guac_display_layer_should_use_webp(guac_display_layer* layer, void* guac_display_worker_thread(void* data) { + /* Thread name display-wrk: one worker in the display pool; encodes and + * sends graphical updates for dirty layer regions. */ + guac_thread_name_set("display-wrk"); + int framerate; int has_outstanding_frames = 0; diff --git a/src/libguac/guacamole/proctitle.h b/src/libguac/guacamole/proctitle.h new file mode 100644 index 0000000000..30a7c0f0d7 --- /dev/null +++ b/src/libguac/guacamole/proctitle.h @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _GUAC_PROCTITLE_H +#define _GUAC_PROCTITLE_H + +/** + * @file proctitle.h + * + * These functions allow guacd and its per-connection child processes to + * show meaningful process and thread names in tools such as `ps`, `top`, + * `htop`, and `gdb`. Process titles can be updated at runtime to identify + * the active connection (for example, `vnc user@example.com:5900`), while + * worker threads can be assigned short descriptive names (for example, + * `display-wrk` or `user-input`). + * + * Process titles are visible through normal process-listing interfaces and + * should be treated as local-observable metadata. Callers must not include + * passwords, tokens, keys, or other secrets. + * + * Process title support operates on a single argv/environ memory region + * captured once at process startup by guac_process_title_init(). The captured + * buffer address and size are stored as process-wide static state and + * reused by all subsequent title updates. Writes are serialized with a + * static mutex so callers can invoke from any thread. + * + * Linux exposes argv+environ as a contiguous block whose contents are + * returned through `/proc//cmdline`. Process title updates work by: + * - Capturing that block's address and length at startup. + * - Moving `environ` to a heap copy so `getenv()` continues to work + * after the original storage is overwritten. + * - Reusing the captured block as a writable title buffer, NUL-padding + * any unused bytes. + * + * Thread names are exposed through Linux's thread `comm` mechanism: + * - `prctl(PR_SET_NAME)` updates the calling thread's name + * (15 characters plus NUL). Used by guac_thread_name_set(). + * + * - Writing `/proc/self/task//comm` updates the name of any + * thread in the process. guac_process_title_set() uses this to update + * the main thread so process-oriented tools such as `top` and + * `ps -e` display the active connection. + * + * --- When to call --- + * + * guac_process_title_init: + * Call once from main() before creating threads and before any + * code modifies `environ`. + * + * guac_process_title_set: + * Call any time after initialization. Typically used once per + * child process after the connection type and target are known. + * + * guac_thread_name_set: + * Call at thread startup to assign a descriptive worker name. + */ + +/** + * Recommended buffer size for formatting a process title before passing it + * to guac_process_title_set(). Fits a protocol name, user, host, and port in + * typical deployments. + */ +#define GUAC_PROCESS_TITLE_BUFSIZE 256 + +/** + * Initializes process title support by capturing the writable argv/environ + * region and moving `environ` to the heap. Must be called from main() + * before any other thread starts and before anything else reuses argv or + * environ. Safe to call more than once (subsequent calls no-op) and safe + * with bad args (no-op). + * + * If the heap environ copy fails to allocate, leaves environ in place and + * disables cmdline updates: later guac_process_title_set() calls still update + * the main-thread comm. + * + * @param argc + * The argument count as passed to main(). + * + * @param argv + * The argument vector as passed to main(). + */ +void guac_process_title_init(int argc, char** argv); + +/** + * Updates the process title: both /proc//cmdline (the COMMAND column + * in `ps`/`htop`) and the *main thread's* short comm (visible in `top`, + * `ps -e`). The calling thread's own comm is left alone: use + * guac_thread_name_set() for that. + * + * If guac_process_title_init() was not called successfully, the cmdline part + * is skipped and only the comm write is attempted. + * + * @param title + * The new title. Truncated to fit the captured argv region (cmdline) + * and to 15 chars for the main thread comm. NULL no-ops. + */ +void guac_process_title_set(const char* title); + +/** + * Convenience wrapper around guac_process_title_set() that formats a network + * connection title in the form " [user@]host[:port]". Falls back to + * "unknown-host" when host is NULL or empty, and omits the "user@" or ":port" + * portion when the respective argument is NULL or empty. The port is taken as + * a string so callers with either a numeric or named port can use it; numeric + * ports should be converted with guac_itoa_safe() first. + * + * The username is partially masked before display (e.g. "bbennett" becomes + * "bb****tt") so the full target account name is not exposed in world-readable + * process listings. Short or non-printable-ASCII usernames are masked in full. + * This is obfuscation to reduce casual disclosure, not an access control. + * + * @param protocol + * Short protocol label, e.g. "vnc", "rdp", "ssh". A NULL value no-ops. + * + * @param user + * The connecting username, or NULL/empty to omit the "user@" portion. The + * value is partially masked before it appears in the process title. + * + * @param host + * The target hostname, or NULL/empty to substitute "unknown-host". + * + * @param port + * The target port as a string, or NULL/empty to omit the ":port" portion. + */ +void guac_process_title_set_endpoint(const char* protocol, const char* user, + const char* host, const char* port); + +/** + * Sets the *calling* thread's short name (visible in + * /proc//task//comm, `top -H`, `ps -L`, ...) + * via prctl(PR_SET_NAME). + * + * Thread-safe: the underlying prctl targets only the calling thread. + * + * Note: threads spawned later by the calling thread inherit + * this name as their initial comm. If you call into a library + * that creates its own workers, those workers will appear with + * the parent's name until they rename themselves. + * + * Each call site is preceded by a comment of the form + * "Thread name : ". To list every named thread + * and what it does, search the source. The description may wrap onto + * the following line, so include one line of trailing context: + * + * grep -rn -A1 "Thread name " src/ + * + * @param name + * The new thread name. Truncated to 15 chars + NULL. + */ +void guac_thread_name_set(const char* name); + +#endif diff --git a/src/libguac/proctitle.c b/src/libguac/proctitle.c new file mode 100644 index 0000000000..79ac17fe4e --- /dev/null +++ b/src/libguac/proctitle.c @@ -0,0 +1,372 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "config.h" + +#include "guacamole/proctitle.h" + +#include +#include + +#include +#include +#include +#include +#include + +#ifdef HAVE_PRCTL +#include +#endif + +extern char** environ; + +/** + * The size, in bytes, of the buffer used to hold a thread "comm" name. The + * Linux kernel limits a thread's comm to TASK_COMM_LEN bytes (16), i.e. 15 + * characters plus a null terminator, and silently truncates anything longer, + * so a larger buffer would serve no purpose. + */ +#define GUAC_PROCTITLE_COMM_LENGTH 16 + +/** + * The size, in bytes, of the buffer used to hold the /proc path addressing a + * thread's comm pseudo-file. This must be large enough for + * GUAC_PROCTITLE_COMM_PATH expanded with the widest possible PID. A 64-bit + * PID is at most 20 decimal digits, and the surrounding literal text is 21 + * characters, so 64 bytes leaves comfortable headroom. + */ +#define GUAC_PROCTITLE_COMM_PATH_LENGTH 64 + +/** + * Format string for the /proc path of a thread's comm pseudo-file. The single + * argument is the target thread's TID; for the thread-group leader (the + * process "main" thread) this equals the process PID returned by getpid(). + */ +#define GUAC_PROCTITLE_COMM_PATH "/proc/self/task/%ld/comm" + +/** + * The number of leading and trailing characters of a username preserved by + * mask_username(); the characters in between are replaced with a fixed run of + * asterisks. + */ +#define GUAC_PROCTITLE_USER_REVEAL 2 + +/** + * The fixed asterisk run substituted for the masked (middle) portion of a + * username by mask_username(). Its width is intentionally constant, rather + * than matching the number of characters removed, so the obfuscated form does + * not leak the original username's length. + */ +#define GUAC_PROCTITLE_USER_MASK "****" + +/** + * The minimum username length, in bytes, for which mask_username() reveals the + * leading and trailing characters. Names shorter than twice + * GUAC_PROCTITLE_USER_REVEAL characters would have all (or overlapping) + * characters exposed, so they are masked in their entirety instead. The "+ 1" + * guarantees at least one character is always masked. + */ +#define GUAC_PROCTITLE_USER_MIN_REVEAL (2 * GUAC_PROCTITLE_USER_REVEAL + 1) + +/** + * The size, in bytes, of a buffer guaranteed to hold any string produced by + * mask_username(): GUAC_PROCTITLE_USER_REVEAL characters at each end plus the + * GUAC_PROCTITLE_USER_MASK run (whose sizeof() includes its NUL terminator). + */ +#define GUAC_PROCTITLE_USER_MASKED_BUFSIZE \ + (2 * GUAC_PROCTITLE_USER_REVEAL + sizeof(GUAC_PROCTITLE_USER_MASK)) + +/** + * The inclusive bounds of the printable ASCII range (space through tilde). + * mask_username() treats any username byte outside this range as + * non-printable and masks the username in full. + */ +#define ASCII_PRINTABLE_MIN 0x20 +#define ASCII_PRINTABLE_MAX 0x7E + +/** + * The start of the writable argv/environ area used for process titles. + */ +static char* guac_proctitle_buffer = NULL; + +/** + * The number of bytes available within guac_proctitle_buffer. + */ +static size_t guac_proctitle_buffer_size = 0; + +/** + * Serializes access to the process-global proctitle state: the argv overlay + * buffer and the /proc//task//comm write. Static-initialized so + * the first caller does not race against a missing pthread_mutex_init(). + */ +static pthread_mutex_t guac_proctitle_lock = PTHREAD_MUTEX_INITIALIZER; + +/** + * Updates the main thread's short comm name from any thread in the same + * process, without changing the calling thread's own comm. Writes through + * /proc//task//comm; the main thread is identified by + * TID == TGID == getpid(). This relies on the Linux procfs layout and is a + * no-op on platforms that lack it. + */ +static void guac_main_thread_name_set(const char* name) { + +#ifdef __linux__ + char comm_path[GUAC_PROCTITLE_COMM_PATH_LENGTH]; + char short_name[GUAC_PROCTITLE_COMM_LENGTH]; + FILE* comm_file; + + if (name == NULL) + return; + + memset(short_name, '\0', sizeof(short_name)); + guac_strlcpy(short_name, name, sizeof(short_name)); + + snprintf(comm_path, sizeof(comm_path), GUAC_PROCTITLE_COMM_PATH, + (long) getpid()); + + /* Write the new name to the main thread's comm pseudo-file. The kernel + * accepts a short string (no trailing newline required) and truncates + * to TASK_COMM_LEN. fopen() may fail in restricted environments where + * /proc is unavailable or write access is denied; treat that as a + * silent no-op since process naming is best-effort observability. */ + comm_file = fopen(comm_path, "w"); + if (comm_file == NULL) + return; + + fputs(short_name, comm_file); + fclose(comm_file); +#else + (void) name; +#endif + +} + +void guac_process_title_init(int argc, char** argv) { + + char* buffer_end; + char** copied_environ; + int envc = 0; + int i; + + if (argc <= 0 || argv == NULL || argv[0] == NULL) + return; + + pthread_mutex_lock(&guac_proctitle_lock); + + /* Idempotent: a second init after the buffer is claimed is a no-op. */ + if (guac_proctitle_buffer != NULL) { + pthread_mutex_unlock(&guac_proctitle_lock); + return; + } + + buffer_end = argv[argc - 1] + strlen(argv[argc - 1]) + 1; + + for (i = 0; environ != NULL && environ[i] != NULL; i++) { + + if (buffer_end == environ[i]) + buffer_end = environ[i] + strlen(environ[i]) + 1; + + envc++; + + } + + /* Copy environ to heap so it survives later overwrites of the argv area. + * If any allocation fails, leave the original environ in place and bail + * out without committing partial state. */ + copied_environ = guac_mem_alloc(sizeof(char*), envc + 1); + if (copied_environ == NULL) { + pthread_mutex_unlock(&guac_proctitle_lock); + return; + } + + for (i = 0; i < envc; i++) { + + copied_environ[i] = guac_strdup(environ[i]); + if (copied_environ[i] == NULL) { + while (i > 0) + guac_mem_free(copied_environ[--i]); + guac_mem_free(copied_environ); + pthread_mutex_unlock(&guac_proctitle_lock); + return; + } + + } + copied_environ[envc] = NULL; + + environ = copied_environ; + guac_proctitle_buffer = argv[0]; + guac_proctitle_buffer_size = buffer_end - argv[0]; + + pthread_mutex_unlock(&guac_proctitle_lock); + +} + +void guac_process_title_set(const char* title) { + + size_t title_length; + + if (title == NULL) + return; + + pthread_mutex_lock(&guac_proctitle_lock); + + guac_main_thread_name_set(title); + + if (guac_proctitle_buffer == NULL || guac_proctitle_buffer_size == 0) { + pthread_mutex_unlock(&guac_proctitle_lock); + return; + } + + title_length = strlen(title); + if (title_length >= guac_proctitle_buffer_size) + title_length = guac_proctitle_buffer_size - 1; + + memcpy(guac_proctitle_buffer, title, title_length); + guac_proctitle_buffer[title_length] = '\0'; + + if (title_length + 1 < guac_proctitle_buffer_size) { + memset(guac_proctitle_buffer + title_length + 1, '\0', + guac_proctitle_buffer_size - title_length - 1); + } + + pthread_mutex_unlock(&guac_proctitle_lock); + +} + +/** + * Writes a partially obfuscated form of the given username into the provided + * buffer for inclusion in a process title. The goal is to retain enough of the + * username for an operator to recognize a session at a glance while keeping the + * full target account name out of world-readable process listings + * (/proc//cmdline). + * + * The algorithm is: + * + * 1. A NULL or empty username yields an empty string, so the caller omits + * the "user@" portion of the title entirely. + * + * 2. A username containing any byte outside the printable ASCII range + * (0x20-0x7E) is masked in full (GUAC_PROCTITLE_USER_MASK). This avoids + * splitting a multi-byte UTF-8 codepoint and emitting garbage. + * + * 3. A username shorter than GUAC_PROCTITLE_USER_MIN_REVEAL bytes is masked + * in full, since revealing GUAC_PROCTITLE_USER_REVEAL characters at each + * end would otherwise expose every (or overlapping) character (e.g. + * "root"). + * + * 4. Otherwise the first and last GUAC_PROCTITLE_USER_REVEAL characters are + * preserved and the middle is replaced with the fixed-width run + * GUAC_PROCTITLE_USER_MASK, e.g. "bbennett" -> "bb****tt". The run width + * is constant regardless of username length so the result does not leak + * the original length. + * + * @param user + * The username to obfuscate, which may be NULL. + * + * @param out + * The buffer to receive the NUL-terminated, obfuscated username. Should be + * at least GUAC_PROCTITLE_USER_MASKED_BUFSIZE bytes. + * + * @param out_size + * The size of the out buffer, in bytes. + */ +static void mask_username(const char* user, char* out, size_t out_size) { + + if (user == NULL || *user == '\0') { + out[0] = '\0'; + return; + } + + size_t length = strlen(user); + + /* Reveal the prefix & suffix edges only for sufficiently long, plain + * printable-ASCII usernames; otherwise mask the entire value. */ + int reveal = (length >= GUAC_PROCTITLE_USER_MIN_REVEAL); + for (size_t i = 0; reveal && i < length; i++) { + unsigned char c = (unsigned char) user[i]; + if (c < ASCII_PRINTABLE_MIN || c > ASCII_PRINTABLE_MAX) + reveal = 0; + } + + if (!reveal) { + guac_strlcpy(out, GUAC_PROCTITLE_USER_MASK, out_size); + return; + } + + /* Preserve the first and last GUAC_PROCTITLE_USER_REVEAL characters, + * replacing everything between them with the fixed mask. */ + char prefix[GUAC_PROCTITLE_USER_REVEAL + 1]; + memcpy(prefix, user, GUAC_PROCTITLE_USER_REVEAL); + prefix[GUAC_PROCTITLE_USER_REVEAL] = '\0'; + + const char* suffix = user + length - GUAC_PROCTITLE_USER_REVEAL; + + snprintf(out, out_size, "%s%s%s", prefix, GUAC_PROCTITLE_USER_MASK, suffix); + +} + +void guac_process_title_set_endpoint(const char* protocol, const char* user, + const char* host, const char* port) { + + char title[GUAC_PROCESS_TITLE_BUFSIZE]; + char masked_user[GUAC_PROCTITLE_USER_MASKED_BUFSIZE]; + + if (protocol == NULL) + return; + + /* Fall back to a placeholder host; omit the user and port portions when + * not provided. */ + if (host == NULL || *host == '\0') + host = "unknown-host"; + + /* The username is partially masked before display: it is target account + * metadata exposed in world-readable process listings. */ + mask_username(user, masked_user, sizeof(masked_user)); + + int has_user = (masked_user[0] != '\0'); + int has_port = (port != NULL && *port != '\0'); + + if (has_user && has_port) + snprintf(title, sizeof(title), "%s %s@%s:%s", protocol, masked_user, host, port); + else if (has_user) + snprintf(title, sizeof(title), "%s %s@%s", protocol, masked_user, host); + else if (has_port) + snprintf(title, sizeof(title), "%s %s:%s", protocol, host, port); + else + snprintf(title, sizeof(title), "%s %s", protocol, host); + + guac_process_title_set(title); + +} + +void guac_thread_name_set(const char* name) { + +#ifdef HAVE_PRCTL + char short_name[GUAC_PROCTITLE_COMM_LENGTH]; + + if (name == NULL) + return; + + /* guac_strlcpy() properly terminates short_name[]. */ + guac_strlcpy(short_name, name, sizeof(short_name)); + prctl(PR_SET_NAME, short_name, 0, 0, 0); +#else + (void) name; +#endif + +} diff --git a/src/libguac/socket.c b/src/libguac/socket.c index 2c2cd8743d..4dfc256734 100644 --- a/src/libguac/socket.c +++ b/src/libguac/socket.c @@ -20,6 +20,7 @@ #include "config.h" #include "guacamole/mem.h" #include "guacamole/error.h" +#include "guacamole/proctitle.h" #include "guacamole/protocol.h" #include "guacamole/socket.h" #include "guacamole/timestamp.h" @@ -45,6 +46,10 @@ char __guac_socket_BASE64_CHARACTERS[64] = { static void* __guac_socket_keep_alive_thread(void* data) { + /* Thread name keep-alive: periodically sends keep-alive NOPs on an + * otherwise idle socket. */ + guac_thread_name_set("keep-alive"); + int old_cancelstate; /* Socket keep-alive loop */ diff --git a/src/libguac/tcp.c b/src/libguac/tcp.c index ba797658e6..b4b12ba6eb 100644 --- a/src/libguac/tcp.c +++ b/src/libguac/tcp.c @@ -30,6 +30,11 @@ #include #include +/* Fallback for platforms that do not define EBADFD. */ +#ifndef EBADFD +#define EBADFD EBADF +#endif + int guac_tcp_connect(const char* hostname, const char* port, const int timeout) { int retval; diff --git a/src/libguac/user-handshake.c b/src/libguac/user-handshake.c index 422c941b73..f0aa35ce57 100644 --- a/src/libguac/user-handshake.c +++ b/src/libguac/user-handshake.c @@ -23,6 +23,7 @@ #include "guacamole/client.h" #include "guacamole/error.h" #include "guacamole/parser.h" +#include "guacamole/proctitle.h" #include "guacamole/protocol.h" #include "guacamole/socket.h" #include "guacamole/user.h" @@ -129,6 +130,10 @@ static void guac_user_log_handshake_failure(guac_user* user) { */ static void* guac_user_input_thread(void* data) { + /* Thread name user-input: reads and parses instructions from a single + * user's socket. */ + guac_thread_name_set("user-input"); + guac_user_input_thread_params* params = (guac_user_input_thread_params*) data; diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index e32925d241..af2005f72d 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -176,6 +177,10 @@ struct lws_protocols guac_kubernetes_lws_protocols[] = { */ static void* guac_kubernetes_input_thread(void* data) { + /* Thread name k8s-input: reads user input and forwards it to the + * Kubernetes pod. */ + guac_thread_name_set("k8s-input"); + guac_client* client = (guac_client*) data; guac_kubernetes_client* kubernetes_client = (guac_kubernetes_client*) client->data; @@ -198,6 +203,10 @@ static void* guac_kubernetes_input_thread(void* data) { void* guac_kubernetes_client_thread(void* data) { + /* Thread name k8s-worker: main Kubernetes client thread; manages the + * websocket connection to the pod. */ + guac_thread_name_set("k8s-worker"); + guac_client* client = (guac_client*) data; guac_kubernetes_client* kubernetes_client = (guac_kubernetes_client*) client->data; @@ -214,6 +223,30 @@ void* guac_kubernetes_client_thread(void* data) { goto fail; } + const char* kubernetes_namespace = settings->kubernetes_namespace; + char kubernetes_title[GUAC_PROCESS_TITLE_BUFSIZE]; + + /* Namespace should already be populated by argument parsing, but + * provide fallback. */ + if (kubernetes_namespace == NULL || *kubernetes_namespace == '\0') + kubernetes_namespace = GUAC_KUBERNETES_DEFAULT_NAMESPACE; + + /* Identify the attached Kubernetes target (for example, + * "k8s default/mypod" or "k8s default/mypod/container"). Include the + * container when specified to distinguish multi-container pods. */ + if (settings->kubernetes_container != NULL + && *settings->kubernetes_container != '\0') + snprintf(kubernetes_title, sizeof(kubernetes_title), + "%s %s/%s/%s", GUAC_KUBERNETES_PROCESS_TITLE_NAME, + kubernetes_namespace, settings->kubernetes_pod, + settings->kubernetes_container); + else + snprintf(kubernetes_title, sizeof(kubernetes_title), + "%s %s/%s", GUAC_KUBERNETES_PROCESS_TITLE_NAME, + kubernetes_namespace, settings->kubernetes_pod); + + guac_process_title_set(kubernetes_title); + /* Generate endpoint for attachment URL */ if (guac_kubernetes_endpoint_uri(endpoint_path, sizeof(endpoint_path), settings->kubernetes_namespace, diff --git a/src/protocols/kubernetes/settings.h b/src/protocols/kubernetes/settings.h index 193f870c33..50e8061224 100644 --- a/src/protocols/kubernetes/settings.h +++ b/src/protocols/kubernetes/settings.h @@ -30,6 +30,12 @@ */ #define GUAC_KUBERNETES_DEFAULT_PORT 8080 +/** + * The protocol label included in the process title (the first argument passed + * to guac_process_title_set_endpoint()), as seen in `ps`/`top`. + */ +#define GUAC_KUBERNETES_PROCESS_TITLE_NAME "k8s" + /** * The name of the Kubernetes namespace that should be used by default if no * specific Kubernetes namespace is provided. diff --git a/src/protocols/rdp/channels/audio-input/audio-buffer.c b/src/protocols/rdp/channels/audio-input/audio-buffer.c index 38d5b7f1e7..24b3d05f8b 100644 --- a/src/protocols/rdp/channels/audio-input/audio-buffer.c +++ b/src/protocols/rdp/channels/audio-input/audio-buffer.c @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -217,6 +218,10 @@ static void guac_rdp_audio_buffer_wait(guac_rdp_audio_buffer* audio_buffer) { */ static void* guac_rdp_audio_buffer_flush_thread(void* data) { + /* Thread name rdp-audio: flushes buffered audio input to the RDP + * server at the negotiated rate. */ + guac_thread_name_set("rdp-audio"); + guac_rdp_audio_buffer* audio_buffer = (guac_rdp_audio_buffer*) data; while (!audio_buffer->stopping) { diff --git a/src/protocols/rdp/print-job.c b/src/protocols/rdp/print-job.c index eda0275a3e..d899598b6f 100644 --- a/src/protocols/rdp/print-job.c +++ b/src/protocols/rdp/print-job.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -392,6 +393,10 @@ static pid_t guac_rdp_create_filter_process(guac_client* client, */ static void* guac_rdp_print_job_output_thread(void* data) { + /* Thread name rdp-print: streams output from the RDP print filter + * process to the client as a downloadable file. */ + guac_thread_name_set("rdp-print"); + int length; char buffer[6048]; diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index 3342079d04..23ed9bc45e 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -57,6 +57,7 @@ #include #include #include +#include #include #include #include @@ -732,10 +733,20 @@ static int guac_rdp_handle_connection(guac_client* client) { void* guac_rdp_client_thread(void* data) { + /* Thread name rdp-worker: main RDP client thread; runs the FreeRDP + * connection and event loop. */ + guac_thread_name_set("rdp-worker"); + guac_client* client = (guac_client*) data; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; guac_rdp_settings* settings = rdp_client->settings; + char rdp_port[GUAC_USHORT_STRING_BUFSIZE]; + if (guac_itoa_safe(rdp_port, sizeof(rdp_port), settings->port) < 1) + rdp_port[0] = '\0'; + guac_process_title_set_endpoint(GUAC_RDP_PROCESS_TITLE_NAME, + settings->username, settings->hostname, rdp_port); + /* If Wake-on-LAN is enabled, attempt to wake. */ if (settings->wol_send_packet) { diff --git a/src/protocols/rdp/settings.h b/src/protocols/rdp/settings.h index f23cefbe3c..8ce5f0308c 100644 --- a/src/protocols/rdp/settings.h +++ b/src/protocols/rdp/settings.h @@ -42,6 +42,12 @@ */ #define RDP_DEFAULT_PORT 3389 +/** + * The protocol label included in the process title (the first argument passed + * to guac_process_title_set_endpoint()), as seen in `ps`/`top`. + */ +#define GUAC_RDP_PROCESS_TITLE_NAME "rdp" + /** * The default SFTP connection timeout, in seconds. */ diff --git a/src/protocols/ssh/settings.h b/src/protocols/ssh/settings.h index 606177c546..e3633f124d 100644 --- a/src/protocols/ssh/settings.h +++ b/src/protocols/ssh/settings.h @@ -30,6 +30,12 @@ */ #define GUAC_SSH_DEFAULT_PORT "22" +/** + * The protocol label included in the process title (the first argument passed + * to guac_process_title_set_endpoint()), as seen in `ps`/`top`. + */ +#define GUAC_SSH_PROCESS_TITLE_NAME "ssh" + /** * The default number of seconds to attempt a connection to the SSH/SFTP * server before giving up. diff --git a/src/protocols/ssh/ssh.c b/src/protocols/ssh/ssh.c index 508492b740..cd91bd9f5b 100644 --- a/src/protocols/ssh/ssh.c +++ b/src/protocols/ssh/ssh.c @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -201,6 +202,10 @@ static char* guac_ssh_get_credential(guac_client *client, char* cred_name) { void* ssh_input_thread(void* data) { + /* Thread name ssh-stdin: reads terminal STDIN and forwards it to the + * SSH server. */ + guac_thread_name_set("ssh-stdin"); + guac_client* client = (guac_client*) data; guac_ssh_client* ssh_client = (guac_ssh_client*) client->data; @@ -226,6 +231,10 @@ void* ssh_input_thread(void* data) { void* ssh_client_thread(void* data) { + /* Thread name ssh-worker: main SSH client thread; runs the SSH session + * and drives the terminal. */ + guac_thread_name_set("ssh-worker"); + guac_client* client = (guac_client*) data; guac_ssh_client* ssh_client = (guac_ssh_client*) client->data; guac_ssh_settings* settings = ssh_client->settings; @@ -343,6 +352,11 @@ void* ssh_client_thread(void* data) { /* Ensure connection is kept alive during lengthy connects */ guac_socket_require_keep_alive(client->socket); + const char* ssh_port = settings->port != NULL + ? settings->port : GUAC_SSH_DEFAULT_PORT; + guac_process_title_set_endpoint(GUAC_SSH_PROCESS_TITLE_NAME, + settings->username, settings->hostname, ssh_port); + /* Open SSH session */ ssh_client->session = guac_common_ssh_create_session(client, settings->hostname, settings->port, ssh_client->user, diff --git a/src/protocols/telnet/settings.h b/src/protocols/telnet/settings.h index 43432619d0..f341fc6aee 100644 --- a/src/protocols/telnet/settings.h +++ b/src/protocols/telnet/settings.h @@ -31,6 +31,12 @@ */ #define GUAC_TELNET_DEFAULT_PORT "23" +/** + * The protocol label included in the process title (the first argument passed + * to guac_process_title_set_endpoint()), as seen in `ps`/`top`. + */ +#define GUAC_TELNET_PROCESS_TITLE_NAME "telnet" + /** * The default number of seconds to wait for a successful connection before * timing out. diff --git a/src/protocols/telnet/telnet.c b/src/protocols/telnet/telnet.c index 71635e6ee2..948cdc4a06 100644 --- a/src/protocols/telnet/telnet.c +++ b/src/protocols/telnet/telnet.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -358,6 +359,10 @@ static void __guac_telnet_event_handler(telnet_t* telnet, telnet_event_t* event, */ static void* __guac_telnet_input_thread(void* data) { + /* Thread name telnet-stdin: reads terminal STDIN and forwards it to the + * telnet server. */ + guac_thread_name_set("telnet-stdin"); + guac_client* client = (guac_client*) data; guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; @@ -491,6 +496,10 @@ static int __guac_telnet_wait(int socket_fd) { void* guac_telnet_client_thread(void* data) { + /* Thread name telnet-worker: main telnet client thread; runs the + * telnet session and event loop. */ + guac_thread_name_set("telnet-worker"); + guac_client* client = (guac_client*) data; guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; guac_telnet_settings* settings = telnet_client->settings; @@ -499,6 +508,11 @@ void* guac_telnet_client_thread(void* data) { char buffer[8192]; int wait_result; + const char* telnet_port = settings->port != NULL + ? settings->port : GUAC_TELNET_DEFAULT_PORT; + guac_process_title_set_endpoint(GUAC_TELNET_PROCESS_TITLE_NAME, + settings->username, settings->hostname, telnet_port); + /* If Wake-on-LAN is enabled, attempt to wake. */ if (settings->wol_send_packet) { diff --git a/src/protocols/vnc/settings.h b/src/protocols/vnc/settings.h index f63cf8076e..ac914f66f7 100644 --- a/src/protocols/vnc/settings.h +++ b/src/protocols/vnc/settings.h @@ -22,6 +22,12 @@ #include +/** + * The protocol label included in the process title (the first argument passed + * to guac_process_title_set_endpoint()), as seen in `ps`/`top`. + */ +#define GUAC_VNC_PROCESS_TITLE_NAME "vnc" + /** * The filename to use for the screen recording, if not specified. */ diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c index 1d8ab0c770..3a1c2479bc 100644 --- a/src/protocols/vnc/vnc.c +++ b/src/protocols/vnc/vnc.c @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -452,10 +453,24 @@ static rfbBool guac_vnc_handle_messages(guac_client* client) { void* guac_vnc_client_thread(void* data) { + /* Thread name vnc-worker: main VNC client thread; runs the libvncclient + * connection and message loop. */ + guac_thread_name_set("vnc-worker"); + guac_client* client = (guac_client*) data; guac_vnc_client* vnc_client = (guac_vnc_client*) client->data; guac_vnc_settings* settings = vnc_client->settings; + /* VNC has no default port (0 == unspecified), so suppress a misleading + * ":0" in the title. */ + char vnc_port[GUAC_USHORT_STRING_BUFSIZE]; + if (settings->port == 0 + || guac_itoa_safe(vnc_port, sizeof(vnc_port), + settings->port) < 1) + vnc_port[0] = '\0'; + guac_process_title_set_endpoint(GUAC_VNC_PROCESS_TITLE_NAME, + settings->username, settings->hostname, vnc_port); + /* If Wake-on-LAN is enabled, attempt to wake. */ if (settings->wol_send_packet) { diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index d6b4004cc6..a8ab2306d7 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -49,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -352,6 +353,10 @@ static void guac_terminal_repaint_default_layer(guac_terminal* terminal, */ void* guac_terminal_thread(void* data) { + /* Thread name terminal: renders the terminal emulator display and + * processes output from the remote. */ + guac_thread_name_set("term-render"); + guac_terminal* terminal = (guac_terminal*) data; guac_client* client = terminal->client;