From a20fff3076d54e9df23ce468603b7171f06e402c Mon Sep 17 00:00:00 2001 From: Ciro Iriarte Date: Sun, 5 Jul 2026 23:08:21 -0300 Subject: [PATCH 01/14] GUACAMOLE-2300: Add IPMI Serial-over-LAN protocol with chassis power management Implement a native `ipmi` protocol module (src/protocols/ipmi/) providing IPMI 2.0 Serial-over-LAN console access via FreeIPMI libipmiconsole, modeled on the terminal-based telnet module (guac_terminal for rendering, scrollback, input; recording and typescript support). Add standard chassis power management via libfreeipmi: power on/off/cycle/ reset/soft-shutdown/diagnostic-interrupt, next-boot device override, chassis identify LED, and power status. These are exposed as pre-connect parameters (power-on-connect, boot-device) applied after the SOL session attaches so the user can watch the system boot, and on demand via an in-terminal control menu (Ctrl+]) with y/N confirmation for destructive actions. Wire the module into the autotools build behind --with-ipmi (ENABLE_IPMI), detecting libipmiconsole and libfreeipmi, and add it to the configure summary. Companion web-UI work is tracked in ciroiriarte/guacamole-client#1. Implements ciroiriarte/guacamole-server#1. --- Makefile.am | 5 + configure.ac | 36 ++ src/protocols/ipmi/Makefile.am | 69 ++++ src/protocols/ipmi/argv.c | 95 ++++++ src/protocols/ipmi/argv.h | 89 +++++ src/protocols/ipmi/chassis.c | 370 +++++++++++++++++++++ src/protocols/ipmi/chassis.h | 107 ++++++ src/protocols/ipmi/client.c | 139 ++++++++ src/protocols/ipmi/client.h | 34 ++ src/protocols/ipmi/clipboard.c | 77 +++++ src/protocols/ipmi/clipboard.h | 40 +++ src/protocols/ipmi/input.c | 109 ++++++ src/protocols/ipmi/input.h | 43 +++ src/protocols/ipmi/ipmi.c | 391 ++++++++++++++++++++++ src/protocols/ipmi/ipmi.h | 99 ++++++ src/protocols/ipmi/menu.c | 264 +++++++++++++++ src/protocols/ipmi/menu.h | 59 ++++ src/protocols/ipmi/pipe.c | 49 +++ src/protocols/ipmi/pipe.h | 38 +++ src/protocols/ipmi/settings.c | 589 +++++++++++++++++++++++++++++++++ src/protocols/ipmi/settings.h | 396 ++++++++++++++++++++++ src/protocols/ipmi/user.c | 115 +++++++ src/protocols/ipmi/user.h | 35 ++ 23 files changed, 3248 insertions(+) create mode 100644 src/protocols/ipmi/Makefile.am create mode 100644 src/protocols/ipmi/argv.c create mode 100644 src/protocols/ipmi/argv.h create mode 100644 src/protocols/ipmi/chassis.c create mode 100644 src/protocols/ipmi/chassis.h create mode 100644 src/protocols/ipmi/client.c create mode 100644 src/protocols/ipmi/client.h create mode 100644 src/protocols/ipmi/clipboard.c create mode 100644 src/protocols/ipmi/clipboard.h create mode 100644 src/protocols/ipmi/input.c create mode 100644 src/protocols/ipmi/input.h create mode 100644 src/protocols/ipmi/ipmi.c create mode 100644 src/protocols/ipmi/ipmi.h create mode 100644 src/protocols/ipmi/menu.c create mode 100644 src/protocols/ipmi/menu.h create mode 100644 src/protocols/ipmi/pipe.c create mode 100644 src/protocols/ipmi/pipe.h create mode 100644 src/protocols/ipmi/settings.c create mode 100644 src/protocols/ipmi/settings.h create mode 100644 src/protocols/ipmi/user.c create mode 100644 src/protocols/ipmi/user.h diff --git a/Makefile.am b/Makefile.am index 6ce3814a28..87c37090b8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -39,6 +39,7 @@ DIST_SUBDIRS = \ src/protocols/rdp \ src/protocols/ssh \ src/protocols/telnet \ + src/protocols/ipmi \ src/protocols/vnc SUBDIRS = \ @@ -73,6 +74,10 @@ if ENABLE_TELNET SUBDIRS += src/protocols/telnet endif +if ENABLE_IPMI +SUBDIRS += src/protocols/ipmi +endif + if ENABLE_VNC SUBDIRS += src/protocols/vnc endif diff --git a/configure.ac b/configure.ac index 2f2560dd8d..8e11b6a063 100644 --- a/configure.ac +++ b/configure.ac @@ -1304,6 +1304,38 @@ AM_CONDITIONAL([ENABLE_TELNET], [test "x${have_libtelnet}" = "xyes" \ AC_SUBST(TELNET_LIBS) +# +# FreeIPMI (libipmiconsole + libfreeipmi) +# + +have_freeipmi=disabled +IPMI_LIBS= +IPMI_CFLAGS= +AC_ARG_WITH([ipmi], + [AS_HELP_STRING([--with-ipmi], + [support IPMI Serial-over-LAN @<:@default=check@:>@])], + [], + [with_ipmi=check]) + +if test "x$with_ipmi" != "xno" +then + have_freeipmi=yes + + # The SOL console is driven by libipmiconsole, while chassis power + # management is driven by libfreeipmi. + AC_CHECK_HEADER([ipmiconsole.h],, [have_freeipmi=no]) + AC_CHECK_LIB([ipmiconsole], [ipmiconsole_ctx_create], + [IPMI_LIBS="$IPMI_LIBS -lipmiconsole"], [have_freeipmi=no]) + AC_CHECK_LIB([freeipmi], [ipmi_ctx_create], + [IPMI_LIBS="$IPMI_LIBS -lfreeipmi"], [have_freeipmi=no]) +fi + +AM_CONDITIONAL([ENABLE_IPMI], [test "x${have_freeipmi}" = "xyes" \ + -a "x${have_terminal}" = "xyes"]) + +AC_SUBST(IPMI_CFLAGS) +AC_SUBST(IPMI_LIBS) + # # libwebp # @@ -1489,6 +1521,7 @@ AC_CONFIG_FILES([Makefile src/protocols/rdp/tests/Makefile src/protocols/ssh/Makefile src/protocols/telnet/Makefile + src/protocols/ipmi/Makefile src/protocols/vnc/Makefile]) AC_OUTPUT @@ -1500,6 +1533,7 @@ AM_COND_IF([ENABLE_KUBERNETES], [build_kubernetes=yes], [build_kubernetes=no]) AM_COND_IF([ENABLE_RDP], [build_rdp=yes], [build_rdp=no]) AM_COND_IF([ENABLE_SSH], [build_ssh=yes], [build_ssh=no]) AM_COND_IF([ENABLE_TELNET], [build_telnet=yes], [build_telnet=no]) +AM_COND_IF([ENABLE_IPMI], [build_ipmi=yes], [build_ipmi=no]) AM_COND_IF([ENABLE_VNC], [build_vnc=yes], [build_vnc=no]) # @@ -1549,6 +1583,7 @@ $PACKAGE_NAME version $PACKAGE_VERSION libssl .............. ${have_ssl} libswscale .......... ${have_libswscale} libtelnet ........... ${have_libtelnet} + freeipmi ............ ${have_freeipmi} libVNCServer ........ ${have_libvncserver} libvorbis ........... ${have_vorbis} libpulse ............ ${have_pulse} @@ -1562,6 +1597,7 @@ $PACKAGE_NAME version $PACKAGE_VERSION RDP ........... ${build_rdp} SSH ........... ${build_ssh} Telnet ........ ${build_telnet} + IPMI .......... ${build_ipmi} VNC ........... ${build_vnc} Services / tools: diff --git a/src/protocols/ipmi/Makefile.am b/src/protocols/ipmi/Makefile.am new file mode 100644 index 0000000000..1f654a917b --- /dev/null +++ b/src/protocols/ipmi/Makefile.am @@ -0,0 +1,69 @@ +# +# 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. +# +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# + +AUTOMAKE_OPTIONS = foreign +ACLOCAL_AMFLAGS = -I m4 + +lib_LTLIBRARIES = libguac-client-ipmi.la + +libguac_client_ipmi_la_SOURCES = \ + argv.c \ + chassis.c \ + client.c \ + clipboard.c \ + input.c \ + ipmi.c \ + menu.c \ + pipe.c \ + settings.c \ + user.c + +noinst_HEADERS = \ + argv.h \ + chassis.h \ + client.h \ + clipboard.h \ + input.h \ + ipmi.h \ + menu.h \ + pipe.h \ + settings.h \ + user.h + +libguac_client_ipmi_la_CFLAGS = \ + -Werror -Wall -Iinclude \ + @LIBGUAC_INCLUDE@ \ + @IPMI_CFLAGS@ \ + @TERMINAL_INCLUDE@ + +libguac_client_ipmi_la_LIBADD = \ + @COMMON_LTLIB@ \ + @LIBGUAC_LTLIB@ \ + @TERMINAL_LTLIB@ + +libguac_client_ipmi_la_LDFLAGS = \ + -version-info 0:0:0 \ + @PTHREAD_LIBS@ \ + @IPMI_LIBS@ diff --git a/src/protocols/ipmi/argv.c b/src/protocols/ipmi/argv.c new file mode 100644 index 0000000000..4b84565a96 --- /dev/null +++ b/src/protocols/ipmi/argv.c @@ -0,0 +1,95 @@ +/* + * 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 "argv.h" +#include "ipmi.h" +#include "terminal/terminal.h" + +#include +#include +#include + +#include +#include +#include + +int guac_ipmi_argv_callback(guac_user* user, const char* mimetype, + const char* name, const char* value, void* data) { + + guac_client* client = user->client; + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + guac_terminal* terminal = ipmi_client->term; + + /* Skip if terminal not yet ready */ + if (terminal == NULL) + return 0; + + /* Update color scheme */ + if (strcmp(name, GUAC_IPMI_ARGV_COLOR_SCHEME) == 0) + guac_terminal_apply_color_scheme(terminal, value); + + /* Update font name */ + else if (strcmp(name, GUAC_IPMI_ARGV_FONT_NAME) == 0) + guac_terminal_apply_font(terminal, value, -1, 0); + + /* Update only if font size is sane */ + else if (strcmp(name, GUAC_IPMI_ARGV_FONT_SIZE) == 0) { + int size = atoi(value); + if (size > 0) + guac_terminal_apply_font(terminal, NULL, size, + ipmi_client->settings->resolution); + } + + return 0; + +} + +void* guac_ipmi_send_current_argv(guac_user* user, void* data) { + + /* Defer to the batch handler, using the user's socket to send the data */ + guac_ipmi_send_current_argv_batch(user->client, user->socket); + + return NULL; + +} + +void guac_ipmi_send_current_argv_batch( + guac_client* client, guac_socket* socket) { + + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + guac_terminal* terminal = ipmi_client->term; + + /* Send current color scheme */ + guac_client_stream_argv(client, socket, "text/plain", + GUAC_IPMI_ARGV_COLOR_SCHEME, + guac_terminal_get_color_scheme(terminal)); + + /* Send current font name */ + guac_client_stream_argv(client, socket, "text/plain", + GUAC_IPMI_ARGV_FONT_NAME, + guac_terminal_get_font_name(terminal)); + + /* Send current font size */ + char font_size[64]; + sprintf(font_size, "%i", guac_terminal_get_font_size(terminal)); + guac_client_stream_argv(client, socket, "text/plain", + GUAC_IPMI_ARGV_FONT_SIZE, font_size); + +} diff --git a/src/protocols/ipmi/argv.h b/src/protocols/ipmi/argv.h new file mode 100644 index 0000000000..fef15cf060 --- /dev/null +++ b/src/protocols/ipmi/argv.h @@ -0,0 +1,89 @@ +/* + * 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_IPMI_ARGV_H +#define GUAC_IPMI_ARGV_H + +#include +#include + +/** + * The name of the parameter that specifies/updates the color scheme used by + * the terminal emulator. + */ +#define GUAC_IPMI_ARGV_COLOR_SCHEME "color-scheme" + +/** + * The name of the parameter that specifies/updates the name of the font used + * by the terminal emulator. + */ +#define GUAC_IPMI_ARGV_FONT_NAME "font-name" + +/** + * The name of the parameter that specifies/updates the font size used by the + * terminal emulator. + */ +#define GUAC_IPMI_ARGV_FONT_SIZE "font-size" + +/** + * Handles a received argument value from a Guacamole "argv" instruction, + * updating the given connection parameter. + */ +guac_argv_callback guac_ipmi_argv_callback; + +/** + * Sends the current values of all non-sensitive parameters which may be set + * while the connection is running to the given user. Note that the user + * receiving these values will not necessarily be able to set new values + * themselves if their connection is read-only. This function can be used as + * the callback for guac_client_foreach_user() and guac_client_for_owner() + * + * @param user + * The user that should receive the values of all non-sensitive parameters + * which may be set while the connection is running. + * + * @param data + * The guac_ipmi_client instance associated with the current connection. + * + * @return + * Always NULL. + */ +void* guac_ipmi_send_current_argv(guac_user* user, void* data); + +/** + * Sends the current values of all non-sensitive parameters which may be set + * while the connection is running to the users associated with the provided + * socket. Note that the users receiving these values will not necessarily be + * able to set new values themselves if their connection is read-only. + * + * @param client + * The client associated with the users that should receive the values of + * all non-sensitive parameters which may be set while the connection is + * running. + * + * @param socket + * The socket to send the arguments to the batch of users along. + * + * @return + * Always NULL. + */ +void guac_ipmi_send_current_argv_batch( + guac_client* client, guac_socket* socket); + +#endif diff --git a/src/protocols/ipmi/chassis.c b/src/protocols/ipmi/chassis.c new file mode 100644 index 0000000000..6fca58d56f --- /dev/null +++ b/src/protocols/ipmi/chassis.c @@ -0,0 +1,370 @@ +/* + * 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 "chassis.h" +#include "ipmi.h" +#include "settings.h" + +#include + +#include + +#include +#include +#include + +/** + * The IPMI session timeout, in milliseconds, used for the short-lived + * out-of-band sessions opened to issue chassis commands. Chassis commands are + * simple request/response operations, so a bounded timeout (rather than the + * library default) is used to limit how long a chassis command can stall when + * the BMC is slow or unreachable. This matters because chassis commands + * triggered from the in-terminal control menu run synchronously within the + * user input handler. + */ +#define GUAC_IPMI_CHASSIS_SESSION_TIMEOUT 8000 + +/** + * The IPMI packet retransmission timeout, in milliseconds, used for chassis + * command sessions. + */ +#define GUAC_IPMI_CHASSIS_RETRANSMISSION_TIMEOUT 1000 + +/** + * Maps a guac_ipmi_privilege_level to the corresponding libfreeipmi + * IPMI_PRIVILEGE_LEVEL_* constant. + * + * @param privilege_level + * The privilege level to map. + * + * @return + * The equivalent IPMI_PRIVILEGE_LEVEL_* constant. + */ +static uint8_t guac_ipmi_map_privilege_level( + guac_ipmi_privilege_level privilege_level) { + + switch (privilege_level) { + case GUAC_IPMI_PRIVILEGE_USER: + return IPMI_PRIVILEGE_LEVEL_USER; + case GUAC_IPMI_PRIVILEGE_OPERATOR: + return IPMI_PRIVILEGE_LEVEL_OPERATOR; + case GUAC_IPMI_PRIVILEGE_ADMIN: + default: + return IPMI_PRIVILEGE_LEVEL_ADMIN; + } + +} + +/** + * Maps a guac_ipmi_power_action to the corresponding libfreeipmi + * IPMI_CHASSIS_CONTROL_* constant. + * + * @param action + * The power action to map. Must not be GUAC_IPMI_POWER_NONE. + * + * @return + * The equivalent IPMI_CHASSIS_CONTROL_* constant. + */ +static uint8_t guac_ipmi_map_power_action(guac_ipmi_power_action action) { + + switch (action) { + case GUAC_IPMI_POWER_ON: + return IPMI_CHASSIS_CONTROL_POWER_UP; + case GUAC_IPMI_POWER_OFF: + return IPMI_CHASSIS_CONTROL_POWER_DOWN; + case GUAC_IPMI_POWER_CYCLE: + return IPMI_CHASSIS_CONTROL_POWER_CYCLE; + case GUAC_IPMI_POWER_RESET: + return IPMI_CHASSIS_CONTROL_HARD_RESET; + case GUAC_IPMI_POWER_SOFT_SHUTDOWN: + return IPMI_CHASSIS_CONTROL_INITIATE_SOFT_SHUTDOWN; + case GUAC_IPMI_POWER_DIAGNOSTIC_INTERRUPT: + return IPMI_CHASSIS_CONTROL_PULSE_DIAGNOSTIC_INTERRUPT; + default: + return IPMI_CHASSIS_CONTROL_POWER_DOWN; + } + +} + +/** + * Maps a guac_ipmi_boot_device to the corresponding libfreeipmi + * IPMI_SYSTEM_BOOT_OPTION_BOOT_FLAG_BOOT_DEVICE_FORCE_* constant. + * + * @param device + * The boot device to map. Must not be GUAC_IPMI_BOOT_NONE. + * + * @return + * The equivalent boot device constant. + */ +static uint8_t guac_ipmi_map_boot_device(guac_ipmi_boot_device device) { + + switch (device) { + case GUAC_IPMI_BOOT_PXE: + return IPMI_SYSTEM_BOOT_OPTION_BOOT_FLAG_BOOT_DEVICE_FORCE_PXE; + case GUAC_IPMI_BOOT_DISK: + return IPMI_SYSTEM_BOOT_OPTION_BOOT_FLAG_BOOT_DEVICE_FORCE_HARD_DRIVE; + case GUAC_IPMI_BOOT_CDROM: + return IPMI_SYSTEM_BOOT_OPTION_BOOT_FLAG_BOOT_DEVICE_FORCE_CD_DVD; + case GUAC_IPMI_BOOT_BIOS: + return IPMI_SYSTEM_BOOT_OPTION_BOOT_FLAG_BOOT_DEVICE_FORCE_BIOS_SETUP; + default: + return IPMI_SYSTEM_BOOT_OPTION_BOOT_FLAG_BOOT_DEVICE_NO_OVERRIDE; + } + +} + +/** + * Opens a short-lived IPMI 2.0 out-of-band session against the BMC described + * by the given client's settings. The returned context must be closed with + * ipmi_ctx_close() and freed with ipmi_ctx_destroy() when no longer needed. + * + * @param client + * The guac_client whose settings describe the BMC to connect to. + * + * @return + * A connected ipmi_ctx_t on success, or NULL on failure (with an error + * logged on behalf of the client). + */ +static ipmi_ctx_t guac_ipmi_chassis_connect(guac_client* client) { + + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + guac_ipmi_settings* settings = ipmi_client->settings; + + ipmi_ctx_t ctx = ipmi_ctx_create(); + if (ctx == NULL) { + guac_client_log(client, GUAC_LOG_ERROR, + "Unable to allocate IPMI context for chassis command."); + return NULL; + } + + /* The BMC key (K_g) is optional; its length is required separately as it + * may legitimately contain null bytes. */ + const unsigned char* k_g = (const unsigned char*) settings->k_g; + unsigned int k_g_len = settings->k_g != NULL ? strlen(settings->k_g) : 0; + + /* Open an IPMI 2.0 (RMCP+) out-of-band session. Session and retransmission + * timeouts of 0 request the library defaults. */ + if (ipmi_ctx_open_outofband_2_0(ctx, + settings->hostname, + settings->username, + settings->password, + k_g, k_g_len, + guac_ipmi_map_privilege_level(settings->privilege_level), + (uint8_t) settings->cipher_suite, + GUAC_IPMI_CHASSIS_SESSION_TIMEOUT, + GUAC_IPMI_CHASSIS_RETRANSMISSION_TIMEOUT, + 0 /* workaround flags */, + 0 /* flags */) < 0) { + guac_client_log(client, GUAC_LOG_ERROR, + "Unable to open IPMI session for chassis command: %s", + ipmi_ctx_errormsg(ctx)); + ipmi_ctx_destroy(ctx); + return NULL; + } + + return ctx; + +} + +/** + * Closes and destroys the given IPMI context. + * + * @param ctx + * The context to close and destroy. + */ +static void guac_ipmi_chassis_disconnect(ipmi_ctx_t ctx) { + ipmi_ctx_close(ctx); + ipmi_ctx_destroy(ctx); +} + +int guac_ipmi_chassis_power(guac_client* client, + guac_ipmi_power_action action) { + + /* A request for no action always succeeds */ + if (action == GUAC_IPMI_POWER_NONE) + return 0; + + ipmi_ctx_t ctx = guac_ipmi_chassis_connect(client); + if (ctx == NULL) + return 1; + + int result = 1; + fiid_obj_t obj_cmd_rs = fiid_obj_create(tmpl_cmd_chassis_control_rs); + if (obj_cmd_rs != NULL) { + + if (ipmi_cmd_chassis_control(ctx, + guac_ipmi_map_power_action(action), obj_cmd_rs) < 0) + guac_client_log(client, GUAC_LOG_ERROR, + "Chassis control command failed: %s", + ipmi_ctx_errormsg(ctx)); + else + result = 0; + + fiid_obj_destroy(obj_cmd_rs); + } + + guac_ipmi_chassis_disconnect(ctx); + return result; + +} + +int guac_ipmi_chassis_set_boot_device(guac_client* client, + guac_ipmi_boot_device device, bool persistent) { + + /* A request for no override always succeeds */ + if (device == GUAC_IPMI_BOOT_NONE) + return 0; + + ipmi_ctx_t ctx = guac_ipmi_chassis_connect(client); + if (ctx == NULL) + return 1; + + int result = 1; + fiid_obj_t obj_cmd_rs = + fiid_obj_create(tmpl_cmd_set_system_boot_options_rs); + if (obj_cmd_rs != NULL) { + + /* Issue a boot flags override. All boot-flag fields other than the + * boot device, persistence, and "valid" markers are left at their + * default/disabled (0) values. */ + if (ipmi_cmd_set_system_boot_options_boot_flags(ctx, + IPMI_SYSTEM_BOOT_OPTIONS_PARAMETER_VALID_UNLOCKED, + IPMI_SYSTEM_BOOT_OPTION_BOOT_FLAG_BOOT_TYPE_PC_COMPATIBLE, + persistent + ? IPMI_SYSTEM_BOOT_OPTION_BOOT_FLAG_VALID_PERSISTENTLY + : IPMI_SYSTEM_BOOT_OPTION_BOOT_FLAG_VALID_FOR_NEXT_BOOT, + IPMI_SYSTEM_BOOT_OPTION_BOOT_FLAG_VALID, + 0 /* lock out reset button */, + 0 /* screen blank */, + guac_ipmi_map_boot_device(device), + 0 /* lock keyboard */, + 0 /* CMOS clear */, + IPMI_SYSTEM_BOOT_OPTION_BOOT_FLAG_CONSOLE_REDIRECTION_DEFAULT, + 0 /* lock out sleep button */, + 0 /* user password bypass */, + 0 /* force progress event traps */, + IPMI_SYSTEM_BOOT_OPTION_BOOT_FLAG_FIRMWARE_BIOS_VERBOSITY_DEFAULT, + 0 /* lock out via power button */, + 0 /* BIOS mux control override (recommended setting) */, + 0 /* BIOS shared mode override (recommended setting) */, + 0 /* device instance selector */, + obj_cmd_rs) < 0) + guac_client_log(client, GUAC_LOG_ERROR, + "Set system boot options command failed: %s", + ipmi_ctx_errormsg(ctx)); + else + result = 0; + + fiid_obj_destroy(obj_cmd_rs); + } + + guac_ipmi_chassis_disconnect(ctx); + return result; + +} + +int guac_ipmi_chassis_status(guac_client* client, char* buffer, int size) { + + ipmi_ctx_t ctx = guac_ipmi_chassis_connect(client); + if (ctx == NULL) + return 1; + + int result = 1; + fiid_obj_t obj_cmd_rs = fiid_obj_create(tmpl_cmd_get_chassis_status_rs); + if (obj_cmd_rs != NULL) { + + if (ipmi_cmd_get_chassis_status(ctx, obj_cmd_rs) < 0) + guac_client_log(client, GUAC_LOG_ERROR, + "Get chassis status command failed: %s", + ipmi_ctx_errormsg(ctx)); + + else { + + uint64_t power_is_on = 0; + uint64_t power_fault = 0; + uint64_t power_overload = 0; + + /* The power state field is mandatory; treat its absence as a + * failure rather than silently reporting a fabricated "OFF". The + * fault/overload fields are informational and tolerated if + * unreadable. */ + if (fiid_obj_get(obj_cmd_rs, "current_power_state.power_is_on", + &power_is_on) < 0) { + guac_client_log(client, GUAC_LOG_ERROR, + "Chassis status response missing power state."); + } + else { + + fiid_obj_get(obj_cmd_rs, "current_power_state.power_fault", + &power_fault); + fiid_obj_get(obj_cmd_rs, "current_power_state.power_overload", + &power_overload); + + snprintf(buffer, size, "Power: %s%s%s", + power_is_on ? "ON" : "OFF", + power_fault ? " (fault)" : "", + power_overload ? " (overload)" : ""); + + result = 0; + } + } + + fiid_obj_destroy(obj_cmd_rs); + } + + guac_ipmi_chassis_disconnect(ctx); + return result; + +} + +int guac_ipmi_chassis_identify(guac_client* client, int interval, bool force) { + + ipmi_ctx_t ctx = guac_ipmi_chassis_connect(client); + if (ctx == NULL) + return 1; + + int result = 1; + fiid_obj_t obj_cmd_rs = fiid_obj_create(tmpl_cmd_chassis_identify_rs); + if (obj_cmd_rs != NULL) { + + uint8_t interval_value = (uint8_t) interval; + uint8_t force_value = force + ? IPMI_CHASSIS_FORCE_IDENTIFY_ON + : IPMI_CHASSIS_FORCE_IDENTIFY_OFF; + + /* When forcing the LED on indefinitely, the interval is not meaningful + * and is passed as NULL. */ + if (ipmi_cmd_chassis_identify(ctx, + force ? NULL : &interval_value, + &force_value, obj_cmd_rs) < 0) + guac_client_log(client, GUAC_LOG_ERROR, + "Chassis identify command failed: %s", + ipmi_ctx_errormsg(ctx)); + else + result = 0; + + fiid_obj_destroy(obj_cmd_rs); + } + + guac_ipmi_chassis_disconnect(ctx); + return result; + +} diff --git a/src/protocols/ipmi/chassis.h b/src/protocols/ipmi/chassis.h new file mode 100644 index 0000000000..79e3f09f78 --- /dev/null +++ b/src/protocols/ipmi/chassis.h @@ -0,0 +1,107 @@ +/* + * 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_IPMI_CHASSIS_H +#define GUAC_IPMI_CHASSIS_H + +#include "settings.h" + +#include + +#include + +/** + * Performs the given chassis power action against the BMC associated with the + * given client. A short-lived IPMI 2.0 out-of-band session, separate from the + * Serial-over-LAN session, is opened to issue the command and closed + * immediately afterward. + * + * @param client + * The guac_client whose settings describe the BMC to connect to. + * + * @param action + * The power action to perform. GUAC_IPMI_POWER_NONE is a no-op which + * returns success. + * + * @return + * Zero if the action was issued successfully, non-zero otherwise. + */ +int guac_ipmi_chassis_power(guac_client* client, + guac_ipmi_power_action action); + +/** + * Applies a boot device override against the BMC associated with the given + * client, affecting either the next boot only or all subsequent boots. + * + * @param client + * The guac_client whose settings describe the BMC to connect to. + * + * @param device + * The boot device to force. GUAC_IPMI_BOOT_NONE is a no-op which returns + * success. + * + * @param persistent + * Whether the override should persist across boots (true) or apply only to + * the next boot (false). + * + * @return + * Zero if the override was applied successfully, non-zero otherwise. + */ +int guac_ipmi_chassis_set_boot_device(guac_client* client, + guac_ipmi_boot_device device, bool persistent); + +/** + * Retrieves the current chassis power status from the BMC, writing a + * human-readable, single-line summary into the given buffer. + * + * @param client + * The guac_client whose settings describe the BMC to connect to. + * + * @param buffer + * The buffer into which the status summary should be written. + * + * @param size + * The size of the given buffer, in bytes. + * + * @return + * Zero if the status was retrieved successfully, non-zero otherwise. + */ +int guac_ipmi_chassis_status(guac_client* client, char* buffer, int size); + +/** + * Activates the chassis identify LED for the given duration, or indefinitely. + * + * @param client + * The guac_client whose settings describe the BMC to connect to. + * + * @param interval + * The number of seconds the identify LED should remain on. Ignored if + * force is true. + * + * @param force + * Whether the identify LED should remain on indefinitely, ignoring the + * given interval. + * + * @return + * Zero if the identify command was issued successfully, non-zero + * otherwise. + */ +int guac_ipmi_chassis_identify(guac_client* client, int interval, bool force); + +#endif diff --git a/src/protocols/ipmi/client.c b/src/protocols/ipmi/client.c new file mode 100644 index 0000000000..e936c03b9b --- /dev/null +++ b/src/protocols/ipmi/client.c @@ -0,0 +1,139 @@ +/* + * 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 "argv.h" +#include "client.h" +#include "ipmi.h" +#include "settings.h" +#include "terminal/terminal.h" +#include "user.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +/** + * A pending join handler implementation that will synchronize the connection + * state for all pending users prior to them being promoted to full user. + * + * @param client + * The client whose pending users are about to be promoted. + * + * @return + * Always zero. + */ +static int guac_ipmi_join_pending_handler(guac_client* client) { + + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + + /* Synchronize the terminal state to all pending users */ + if (ipmi_client->term != NULL) { + guac_socket* broadcast_socket = client->pending_socket; + guac_terminal_sync_users(ipmi_client->term, client, broadcast_socket); + guac_ipmi_send_current_argv_batch(client, broadcast_socket); + guac_socket_flush(broadcast_socket); + } + + return 0; + +} + +int guac_client_init(guac_client* client) { + + /* Set client args */ + client->args = GUAC_IPMI_CLIENT_ARGS; + + /* Allocate client instance data */ + guac_ipmi_client* ipmi_client = guac_mem_zalloc(sizeof(guac_ipmi_client)); + client->data = ipmi_client; + + /* Init IPMI client */ + ipmi_client->console_fd = -1; + ipmi_client->menu_open = false; + ipmi_client->menu_pending_action = GUAC_IPMI_POWER_NONE; + + /* Set handlers */ + client->join_handler = guac_ipmi_user_join_handler; + client->join_pending_handler = guac_ipmi_join_pending_handler; + client->free_handler = guac_ipmi_client_free_handler; + client->leave_handler = guac_ipmi_user_leave_handler; + + /* Register handlers for argument values that may be sent after the handshake */ + guac_argv_register(GUAC_IPMI_ARGV_COLOR_SCHEME, guac_ipmi_argv_callback, NULL, GUAC_ARGV_OPTION_ECHO); + guac_argv_register(GUAC_IPMI_ARGV_FONT_NAME, guac_ipmi_argv_callback, NULL, GUAC_ARGV_OPTION_ECHO); + guac_argv_register(GUAC_IPMI_ARGV_FONT_SIZE, guac_ipmi_argv_callback, NULL, GUAC_ARGV_OPTION_ECHO); + + /* Set locale and warn if not UTF-8 */ + setlocale(LC_CTYPE, ""); + if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) { + guac_client_log(client, GUAC_LOG_INFO, + "Current locale does not use UTF-8. Some characters may " + "not render correctly."); + } + + /* Success */ + return 0; + +} + +int guac_ipmi_client_free_handler(guac_client* client) { + + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + + /* Close the SOL file descriptor, unblocking the client thread's read loop. + * As the IPMICONSOLE_ENGINE_CLOSE_FD flag is not used, the descriptor is + * owned by us and must be closed here. */ + if (ipmi_client->console_fd != -1) + close(ipmi_client->console_fd); + + /* Clean up recording, if in progress */ + if (ipmi_client->recording != NULL) + guac_recording_free(ipmi_client->recording); + + /* Kill terminal, unblocking the input thread */ + guac_terminal_free(ipmi_client->term); + + /* Wait for and clean up the SOL session, if established */ + if (ipmi_client->console_ctx != NULL) { + pthread_join(ipmi_client->client_thread, NULL); + ipmiconsole_ctx_destroy(ipmi_client->console_ctx); + ipmiconsole_engine_teardown(0); + } + + /* Free settings */ + if (ipmi_client->settings != NULL) + guac_ipmi_settings_free(ipmi_client->settings); + + guac_mem_free(ipmi_client); + return 0; + +} diff --git a/src/protocols/ipmi/client.h b/src/protocols/ipmi/client.h new file mode 100644 index 0000000000..35dbfb5af0 --- /dev/null +++ b/src/protocols/ipmi/client.h @@ -0,0 +1,34 @@ +/* + * 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_IPMI_CLIENT_H +#define GUAC_IPMI_CLIENT_H + +#include "terminal/terminal.h" + +#include +#include + +/** + * Free handler. Required by libguac and called when the guac_client is + * disconnected and must be cleaned up. + */ +guac_client_free_handler guac_ipmi_client_free_handler; + +#endif diff --git a/src/protocols/ipmi/clipboard.c b/src/protocols/ipmi/clipboard.c new file mode 100644 index 0000000000..74ba8b5611 --- /dev/null +++ b/src/protocols/ipmi/clipboard.c @@ -0,0 +1,77 @@ +/* + * 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 "clipboard.h" +#include "ipmi.h" +#include "terminal/terminal.h" + +#include +#include +#include + +int guac_ipmi_clipboard_handler(guac_user* user, guac_stream* stream, + char* mimetype) { + + /* Clear clipboard and prepare for new data */ + guac_client* client = user->client; + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + guac_terminal_clipboard_reset(ipmi_client->term, mimetype); + + /* Set handlers for clipboard stream */ + stream->blob_handler = guac_ipmi_clipboard_blob_handler; + stream->end_handler = guac_ipmi_clipboard_end_handler; + + /* Report clipboard within recording */ + if (ipmi_client->recording != NULL) + guac_recording_report_clipboard_begin(ipmi_client->recording, stream, + mimetype); + + return 0; +} + +int guac_ipmi_clipboard_blob_handler(guac_user* user, guac_stream* stream, + void* data, int length) { + + guac_client* client = user->client; + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + + /* Report clipboard blob within recording */ + if (ipmi_client->recording != NULL) + guac_recording_report_clipboard_blob(ipmi_client->recording, stream, data, length); + + /* Append new data */ + guac_terminal_clipboard_append(ipmi_client->term, data, length); + + return 0; +} + +int guac_ipmi_clipboard_end_handler(guac_user* user, guac_stream* stream) { + + guac_client* client = user->client; + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + + /* Report clipboard stream end within recording */ + if (ipmi_client->recording != NULL) + guac_recording_report_clipboard_end(ipmi_client->recording, stream); + + /* Nothing to do - clipboard is implemented within client */ + + return 0; +} diff --git a/src/protocols/ipmi/clipboard.h b/src/protocols/ipmi/clipboard.h new file mode 100644 index 0000000000..5a371dc9d6 --- /dev/null +++ b/src/protocols/ipmi/clipboard.h @@ -0,0 +1,40 @@ +/* + * 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_IPMI_CLIPBOARD_H +#define GUAC_IPMI_CLIPBOARD_H + +#include + +/** + * Handler for inbound clipboard streams. + */ +guac_user_clipboard_handler guac_ipmi_clipboard_handler; + +/** + * Handler for data received along clipboard streams. + */ +guac_user_blob_handler guac_ipmi_clipboard_blob_handler; + +/** + * Handler for end-of-stream related to clipboard. + */ +guac_user_end_handler guac_ipmi_clipboard_end_handler; + +#endif diff --git a/src/protocols/ipmi/input.c b/src/protocols/ipmi/input.c new file mode 100644 index 0000000000..376419edbd --- /dev/null +++ b/src/protocols/ipmi/input.c @@ -0,0 +1,109 @@ +/* + * 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 "input.h" +#include "ipmi.h" +#include "menu.h" +#include "terminal/terminal.h" + +#include +#include +#include + +#include +#include + +int guac_ipmi_user_mouse_handler(guac_user* user, int x, int y, int mask) { + + guac_client* client = user->client; + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + guac_terminal* term = ipmi_client->term; + + /* Skip if terminal not yet ready */ + if (term == NULL) + return 0; + + /* Report mouse position within recording */ + if (ipmi_client->recording != NULL) + guac_recording_report_mouse(ipmi_client->recording, x, y, mask); + + /* Send mouse event to terminal */ + guac_terminal_send_mouse(term, user, x, y, mask); + + return 0; + +} + +int guac_ipmi_user_key_handler(guac_user* user, int keysym, int pressed) { + + guac_client* client = user->client; + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + guac_terminal* term = ipmi_client->term; + + /* Report key state within recording */ + if (ipmi_client->recording != NULL) + guac_recording_report_key(ipmi_client->recording, keysym, pressed); + + /* Skip if terminal not yet ready */ + if (term == NULL) + return 0; + + /* While the control menu is open, interpret key presses as menu commands + * rather than forwarding them to the serial console. Key releases are + * ignored. */ + if (ipmi_client->menu_open) { + if (pressed) + guac_ipmi_menu_handle_key(client, keysym); + return 0; + } + + /* Open the control menu when the menu key (Ctrl + ']') is pressed */ + if (pressed && keysym == GUAC_IPMI_MENU_KEYSYM + && guac_terminal_get_mod_ctrl(term)) { + guac_ipmi_menu_open(client); + return 0; + } + + /* Send key to terminal */ + guac_terminal_send_key(term, keysym, pressed); + + return 0; + +} + +int guac_ipmi_user_size_handler(guac_user* user, int width, int height) { + + /* Get terminal */ + guac_client* client = user->client; + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + guac_terminal* terminal = ipmi_client->term; + + /* Skip if terminal not yet ready */ + if (terminal == NULL) + return 0; + + /* Resize terminal. The IPMI SOL protocol provides no mechanism to inform + * the remote serial console of a window size change, so only the local + * terminal is resized. */ + guac_terminal_resize(terminal, width, height); + + return 0; + +} diff --git a/src/protocols/ipmi/input.h b/src/protocols/ipmi/input.h new file mode 100644 index 0000000000..b1eac39ba7 --- /dev/null +++ b/src/protocols/ipmi/input.h @@ -0,0 +1,43 @@ +/* + * 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_IPMI_INPUT_H +#define GUAC_IPMI_INPUT_H + +#include + +/** + * Handler for key events. Required by libguac and called whenever key events + * are received. + */ +guac_user_key_handler guac_ipmi_user_key_handler; + +/** + * Handler for mouse events. Required by libguac and called whenever mouse + * events are received. + */ +guac_user_mouse_handler guac_ipmi_user_mouse_handler; + +/** + * Handler for size events. Required by libguac and called whenever the remote + * display (window) is resized. + */ +guac_user_size_handler guac_ipmi_user_size_handler; + +#endif diff --git a/src/protocols/ipmi/ipmi.c b/src/protocols/ipmi/ipmi.c new file mode 100644 index 0000000000..92d2ccdb34 --- /dev/null +++ b/src/protocols/ipmi/ipmi.c @@ -0,0 +1,391 @@ +/* + * 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 "argv.h" +#include "chassis.h" +#include "ipmi.h" +#include "terminal/terminal.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/** + * Write the entire buffer given to the specified file descriptor, retrying the + * write automatically if necessary. + * + * @param fd + * The file descriptor to write to. + * + * @param buffer + * The buffer to write. + * + * @param size + * The number of bytes from the buffer to write. + * + * @return + * The number of bytes written (equal to size) on success, or a value not + * equal to size if an error occurs which prevents all future writes. + */ +static int __guac_ipmi_write_all(int fd, const char* buffer, int size) { + + int remaining = size; + while (remaining > 0) { + + /* Attempt to write data */ + int ret_val; + GUAC_RETRY_EINTR(ret_val, write(fd, buffer, remaining)); + if (ret_val <= 0) + return -1; + + /* If successful, continue with what data remains (if any) */ + remaining -= ret_val; + buffer += ret_val; + + } + + return size; + +} + +/** + * Waits for data on the given file descriptor for up to one second. The return + * value is identical to that of poll(): 0 on timeout, < 0 on error, and > 0 on + * success. + * + * @param console_fd + * The file descriptor to wait for. + * + * @return + * A value greater than zero on success, zero on timeout, and less than + * zero on error. + */ +static int __guac_ipmi_wait(int console_fd) { + + /* Build array of file descriptors */ + struct pollfd fds[] = {{ + .fd = console_fd, + .events = POLLIN, + .revents = 0, + }}; + + int wait_result; + + /* Wait for one second */ + GUAC_RETRY_EINTR(wait_result, poll(fds, 1, 1000)); + + return wait_result; + +} + +/** + * Input thread, started by the main IPMI client thread. This thread + * continuously reads from the terminal's STDIN and transfers all read data to + * the SOL session. + * + * @param data + * The current guac_client instance. + * + * @return + * Always NULL. + */ +static void* __guac_ipmi_input_thread(void* data) { + + /* Thread name ipmi-stdin: reads terminal STDIN and forwards it to the SOL + * session. */ + guac_thread_name_set("ipmi-stdin"); + + guac_client* client = (guac_client*) data; + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + + char buffer[8192]; + int bytes_read; + + /* Forward all data read from the terminal to the serial console */ + while ((bytes_read = guac_terminal_read_stdin(ipmi_client->term, buffer, + sizeof(buffer))) > 0) { + if (__guac_ipmi_write_all(ipmi_client->console_fd, buffer, bytes_read) + != bytes_read) + break; + } + + return NULL; + +} + +/** + * Establishes the IPMI 2.0 Serial-over-LAN session described by the given + * client's settings, blocking until the session has been established or an + * error occurs. The libipmiconsole engine must already have been initialized. + * + * @param client + * The guac_client whose settings describe the SOL session to establish. + * + * @return + * The established libipmiconsole context on success, or NULL on failure + * (with the connection aborted). + */ +static ipmiconsole_ctx_t __guac_ipmi_create_session(guac_client* client) { + + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + guac_ipmi_settings* settings = ipmi_client->settings; + + /* IPMI authentication and protocol configuration. Values of < 0 (or NULL) + * request the libipmiconsole defaults. */ + struct ipmiconsole_ipmi_config ipmi_config = { + .username = settings->username, + .password = settings->password, + .k_g = (unsigned char*) settings->k_g, + .k_g_len = settings->k_g != NULL ? strlen(settings->k_g) : 0, + .privilege_level = settings->privilege_level, + .cipher_suite_id = settings->cipher_suite, + .workaround_flags = 0 + }; + + struct ipmiconsole_protocol_config protocol_config = { + .session_timeout_len = + settings->timeout > 0 ? settings->timeout * 1000 : -1, + .retransmission_timeout_len = -1, + .retransmission_backoff_count = -1, + .keepalive_timeout_len = -1, + .retransmission_keepalive_timeout_len = -1, + .acceptable_packet_errors_count = -1, + .maximum_retransmission_count = -1 + }; + + struct ipmiconsole_engine_config engine_config = { + .engine_flags = 0, + .behavior_flags = 0, + .debug_flags = 0 + }; + + /* Create the SOL context */ + ipmiconsole_ctx_t ctx = ipmiconsole_ctx_create(settings->hostname, + &ipmi_config, &protocol_config, &engine_config); + if (ctx == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "IPMI console context allocation failed."); + return NULL; + } + + /* Select the requested SOL payload instance */ + unsigned int instance = settings->sol_payload_instance; + if (ipmiconsole_ctx_set_config(ctx, + IPMICONSOLE_CTX_CONFIG_OPTION_SOL_PAYLOAD_INSTANCE, + &instance) < 0) + guac_client_log(client, GUAC_LOG_WARNING, "Unable to set SOL payload " + "instance; using the default."); + + /* Establish the SOL session, blocking until it is established or fails. + * libipmiconsole will, by default, attempt to deactivate any pre-existing + * SOL session on the BMC ("SOL already active"). */ + if (ipmiconsole_engine_submit_block(ctx) < 0) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, + "Unable to establish IPMI SOL session: %s", + ipmiconsole_ctx_errormsg(ctx)); + ipmiconsole_ctx_destroy(ctx); + return NULL; + } + + return ctx; + +} + +/** + * Applies any configured pre-connect boot device override and power action, + * now that the SOL session has been established. Establishing the console + * before powering the system on allows the user to observe the system from the + * earliest stages of boot. + * + * @param client + * The guac_client whose settings describe the actions to perform. + */ +static void __guac_ipmi_apply_connect_actions(guac_client* client) { + + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + guac_ipmi_settings* settings = ipmi_client->settings; + + /* Apply any boot device override prior to powering the system on */ + if (settings->boot_device != GUAC_IPMI_BOOT_NONE) { + if (guac_ipmi_chassis_set_boot_device(client, settings->boot_device, + false) == 0) + guac_client_log(client, GUAC_LOG_INFO, + "Applied boot device override."); + } + + /* Perform any requested power action */ + if (settings->power_on_connect != GUAC_IPMI_POWER_NONE) { + if (guac_ipmi_chassis_power(client, settings->power_on_connect) == 0) + guac_client_log(client, GUAC_LOG_INFO, + "Applied power-on-connect action."); + } + +} + +void* guac_ipmi_client_thread(void* data) { + + /* Thread name ipmi-worker: main IPMI client thread; runs the SOL session + * and reads its output into the terminal. */ + guac_thread_name_set("ipmi-worker"); + + guac_client* client = (guac_client*) data; + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + guac_ipmi_settings* settings = ipmi_client->settings; + + pthread_t input_thread; + char buffer[8192]; + int wait_result; + + guac_process_title_set_endpoint(GUAC_IPMI_PROCESS_TITLE_NAME, + settings->username, settings->hostname, settings->port); + + /* Set up screen recording, if requested */ + if (settings->recording_path != NULL) { + ipmi_client->recording = guac_recording_create(client, + settings->recording_path, + settings->recording_name, + settings->create_recording_path, + !settings->recording_exclude_output, + !settings->recording_exclude_mouse, + 0, /* Touch events not supported */ + settings->recording_include_keys, + settings->recording_write_existing, + settings->recording_include_clipboard); + } + + /* Create terminal options with required parameters */ + guac_terminal_options* options = guac_terminal_options_create( + settings->width, settings->height, settings->resolution); + + /* Set optional parameters */ + options->disable_copy = settings->disable_copy; + options->max_scrollback = settings->max_scrollback; + options->font_name = settings->font_name; + options->font_size = settings->font_size; + options->color_scheme = settings->color_scheme; + options->backspace = settings->backspace; + options->linux_console_keys = (strcmp(settings->terminal_type, "linux") == 0); + + /* Create terminal */ + ipmi_client->term = guac_terminal_create(client, options); + + /* Free options struct now that it's been used */ + guac_mem_free(options); + + /* Fail if terminal init failed */ + if (ipmi_client->term == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Terminal initialization failed"); + return NULL; + } + + /* Send current values of exposed arguments to owner only */ + guac_client_for_owner(client, guac_ipmi_send_current_argv, ipmi_client); + + /* Set up typescript, if requested */ + if (settings->typescript_path != NULL) { + guac_terminal_create_typescript(ipmi_client->term, + settings->typescript_path, + settings->typescript_name, + settings->create_typescript_path, + settings->typescript_write_existing); + } + + /* Initialize the libipmiconsole engine */ + if (ipmiconsole_engine_init(0, 0) < 0) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Unable to initialize IPMI console engine."); + return NULL; + } + + /* Establish the SOL session */ + ipmi_client->console_ctx = __guac_ipmi_create_session(client); + if (ipmi_client->console_ctx == NULL) { + /* Already aborted within __guac_ipmi_create_session() */ + ipmiconsole_engine_teardown(0); + return NULL; + } + + /* Retrieve the file descriptor used to read/write the serial console */ + ipmi_client->console_fd = ipmiconsole_ctx_fd(ipmi_client->console_ctx); + if (ipmi_client->console_fd < 0) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, + "Unable to retrieve IPMI SOL file descriptor."); + return NULL; + } + + guac_client_log(client, GUAC_LOG_INFO, "IPMI SOL session established."); + + /* Allow the terminal to begin rendering immediately, before any + * (potentially slow) pre-connect chassis actions are applied. SOL output + * produced during those actions is buffered by the libipmiconsole engine + * and rendered once the read loop below begins. */ + guac_terminal_start(ipmi_client->term); + + /* Apply any configured pre-connect boot/power actions */ + __guac_ipmi_apply_connect_actions(client); + + /* Start input thread */ + if (pthread_create(&(input_thread), NULL, __guac_ipmi_input_thread, + (void*) client)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Unable to start input thread"); + return NULL; + } + + /* While data is available, write SOL output to the terminal */ + while ((wait_result = __guac_ipmi_wait(ipmi_client->console_fd)) >= 0) { + + /* Resume waiting if no data is available */ + if (wait_result == 0) + continue; + + int bytes_read; + GUAC_RETRY_EINTR(bytes_read, read(ipmi_client->console_fd, buffer, + sizeof(buffer))); + if (bytes_read <= 0) + break; + + guac_terminal_write(ipmi_client->term, buffer, bytes_read); + + } + + /* Kill client and wait for input thread to die */ + guac_client_stop(client); + pthread_join(input_thread, NULL); + + guac_client_log(client, GUAC_LOG_INFO, "IPMI connection ended."); + return NULL; + +} diff --git a/src/protocols/ipmi/ipmi.h b/src/protocols/ipmi/ipmi.h new file mode 100644 index 0000000000..9df9e79af0 --- /dev/null +++ b/src/protocols/ipmi/ipmi.h @@ -0,0 +1,99 @@ +/* + * 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_IPMI_H +#define GUAC_IPMI_H + +#include "settings.h" +#include "terminal/terminal.h" + +#include +#include + +#include +#include + +/** + * IPMI-specific client data. + */ +typedef struct guac_ipmi_client { + + /** + * IPMI connection settings. + */ + guac_ipmi_settings* settings; + + /** + * The IPMI client thread. + */ + pthread_t client_thread; + + /** + * The libipmiconsole context managing the Serial-over-LAN session, or NULL + * if no session has been established. + */ + ipmiconsole_ctx_t console_ctx; + + /** + * The file descriptor of the established SOL session, as returned by + * ipmiconsole_ctx_fd(), or -1 if no session has been established. All + * serial console input/output is read from and written to this descriptor. + */ + int console_fd; + + /** + * The terminal which will render all output from the SOL session. + */ + guac_terminal* term; + + /** + * Whether the in-terminal control (power management) menu is currently + * open. While the menu is open, keystrokes are interpreted as menu + * commands rather than forwarded to the serial console. + */ + bool menu_open; + + /** + * When the control menu is awaiting confirmation of a destructive power + * action, the action pending confirmation. GUAC_IPMI_POWER_NONE when no + * confirmation is in progress. + */ + guac_ipmi_power_action menu_pending_action; + + /** + * The in-progress session recording, or NULL if no recording is in + * progress. + */ + guac_recording* recording; + +} guac_ipmi_client; + +/** + * Main IPMI client thread, establishing the Serial-over-LAN session and + * transferring SOL output to the terminal. + * + * @param data + * The guac_client associated with the IPMI connection. + * + * @return + * Always NULL. + */ +void* guac_ipmi_client_thread(void* data); + +#endif diff --git a/src/protocols/ipmi/menu.c b/src/protocols/ipmi/menu.c new file mode 100644 index 0000000000..1b8fbc8ea0 --- /dev/null +++ b/src/protocols/ipmi/menu.c @@ -0,0 +1,264 @@ +/* + * 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 "chassis.h" +#include "ipmi.h" +#include "menu.h" +#include "settings.h" +#include "terminal/terminal.h" + +#include + +#include + +#include +#include +#include + +/** + * The number of seconds the chassis identify LED is activated for when + * selected from the control menu. + */ +#define GUAC_IPMI_MENU_IDENTIFY_INTERVAL 15 + +/** + * Writes the given null-terminated string to the client's terminal as console + * output. + * + * @param client + * The guac_client associated with the IPMI connection. + * + * @param str + * The null-terminated string to write. + */ +static void guac_ipmi_menu_print(guac_client* client, const char* str) { + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + guac_terminal_write(ipmi_client->term, str, strlen(str)); +} + +/** + * Returns a human-readable name for the given power action. + * + * @param action + * The power action to describe. + * + * @return + * A static, human-readable description of the action. + */ +static const char* guac_ipmi_menu_action_name(guac_ipmi_power_action action) { + switch (action) { + case GUAC_IPMI_POWER_ON: return "Power On"; + case GUAC_IPMI_POWER_OFF: return "Power Off"; + case GUAC_IPMI_POWER_CYCLE: return "Power Cycle"; + case GUAC_IPMI_POWER_RESET: return "Hard Reset"; + case GUAC_IPMI_POWER_SOFT_SHUTDOWN: return "Soft Shutdown"; + case GUAC_IPMI_POWER_DIAGNOSTIC_INTERRUPT: return "Diagnostic Interrupt"; + default: return "None"; + } +} + +/** + * Renders the control menu into the terminal. + * + * @param client + * The guac_client associated with the IPMI connection. + */ +static void guac_ipmi_menu_render(guac_client* client) { + guac_ipmi_menu_print(client, + "\r\n" + "\x1B[1m=== IPMI Control Menu ===\x1B[0m\r\n" + " [1] Power On [2] Power Off\r\n" + " [3] Power Cycle [4] Hard Reset\r\n" + " [5] Soft Shutdown [6] Diagnostic Interrupt (NMI)\r\n" + " [s] Power Status [i] Identify (15s)\r\n" + " [b] Send Break [q] Close menu\r\n" + "Select: "); +} + +/** + * Executes the given power action against the BMC, reporting the result to the + * terminal. + * + * @param client + * The guac_client associated with the IPMI connection. + * + * @param action + * The power action to perform. + */ +static void guac_ipmi_menu_do_power(guac_client* client, + guac_ipmi_power_action action) { + + char message[128]; + + snprintf(message, sizeof(message), "\r\n%s: working...\r\n", + guac_ipmi_menu_action_name(action)); + guac_ipmi_menu_print(client, message); + + if (guac_ipmi_chassis_power(client, action) == 0) + guac_ipmi_menu_print(client, "Command sent successfully.\r\n"); + else + guac_ipmi_menu_print(client, "Command FAILED (see server log).\r\n"); + +} + +/** + * Closes the control menu, clearing all menu state. + * + * @param client + * The guac_client associated with the IPMI connection. + */ +static void guac_ipmi_menu_close(guac_client* client) { + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + ipmi_client->menu_open = false; + ipmi_client->menu_pending_action = GUAC_IPMI_POWER_NONE; + guac_ipmi_menu_print(client, "\r\n"); +} + +/** + * Marks the given destructive power action as awaiting confirmation, prompting + * the user. + * + * @param client + * The guac_client associated with the IPMI connection. + * + * @param action + * The action awaiting confirmation. + */ +static void guac_ipmi_menu_confirm(guac_client* client, + guac_ipmi_power_action action) { + + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + ipmi_client->menu_pending_action = action; + + char message[128]; + snprintf(message, sizeof(message), "\r\nConfirm %s? (y/N): ", + guac_ipmi_menu_action_name(action)); + guac_ipmi_menu_print(client, message); + +} + +void guac_ipmi_menu_open(guac_client* client) { + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + ipmi_client->menu_open = true; + ipmi_client->menu_pending_action = GUAC_IPMI_POWER_NONE; + guac_ipmi_menu_render(client); +} + +void guac_ipmi_menu_handle_key(guac_client* client, int keysym) { + + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + + /* If a destructive action is awaiting confirmation, interpret this key as + * the confirmation response. */ + if (ipmi_client->menu_pending_action != GUAC_IPMI_POWER_NONE) { + + guac_ipmi_power_action action = ipmi_client->menu_pending_action; + ipmi_client->menu_pending_action = GUAC_IPMI_POWER_NONE; + + if (keysym == 'y' || keysym == 'Y') + guac_ipmi_menu_do_power(client, action); + else + guac_ipmi_menu_print(client, "\r\nCancelled.\r\n"); + + guac_ipmi_menu_close(client); + return; + } + + switch (keysym) { + + /* Power On is non-destructive and executed directly */ + case '1': + guac_ipmi_menu_do_power(client, GUAC_IPMI_POWER_ON); + guac_ipmi_menu_close(client); + break; + + /* Destructive actions require confirmation */ + case '2': + guac_ipmi_menu_confirm(client, GUAC_IPMI_POWER_OFF); + break; + case '3': + guac_ipmi_menu_confirm(client, GUAC_IPMI_POWER_CYCLE); + break; + case '4': + guac_ipmi_menu_confirm(client, GUAC_IPMI_POWER_RESET); + break; + case '5': + guac_ipmi_menu_confirm(client, GUAC_IPMI_POWER_SOFT_SHUTDOWN); + break; + case '6': + guac_ipmi_menu_confirm(client, GUAC_IPMI_POWER_DIAGNOSTIC_INTERRUPT); + break; + + /* Power status */ + case 's': + case 'S': { + char status[128]; + guac_ipmi_menu_print(client, "\r\nQuerying power status...\r\n"); + if (guac_ipmi_chassis_status(client, status, sizeof(status)) == 0) { + guac_ipmi_menu_print(client, status); + guac_ipmi_menu_print(client, "\r\n"); + } + else + guac_ipmi_menu_print(client, + "Unable to query status (see server log).\r\n"); + guac_ipmi_menu_close(client); + break; + } + + /* Chassis identify LED */ + case 'i': + case 'I': + guac_ipmi_menu_print(client, "\r\nActivating identify LED...\r\n"); + if (guac_ipmi_chassis_identify(client, + GUAC_IPMI_MENU_IDENTIFY_INTERVAL, false) == 0) + guac_ipmi_menu_print(client, "Identify LED activated.\r\n"); + else + guac_ipmi_menu_print(client, + "Unable to activate identify LED (see server log).\r\n"); + guac_ipmi_menu_close(client); + break; + + /* Send a serial break over the active SOL session */ + case 'b': + case 'B': + if (ipmi_client->console_ctx != NULL + && ipmiconsole_ctx_generate_break( + ipmi_client->console_ctx) == 0) + guac_ipmi_menu_print(client, "\r\nBreak sent.\r\n"); + else + guac_ipmi_menu_print(client, "\r\nUnable to send break.\r\n"); + guac_ipmi_menu_close(client); + break; + + /* Close the menu without action */ + case 'q': + case 'Q': + case 0xFF1B: /* Escape */ + guac_ipmi_menu_close(client); + break; + + /* Ignore any other key, leaving the menu open */ + default: + break; + + } + +} diff --git a/src/protocols/ipmi/menu.h b/src/protocols/ipmi/menu.h new file mode 100644 index 0000000000..3aa5d9f501 --- /dev/null +++ b/src/protocols/ipmi/menu.h @@ -0,0 +1,59 @@ +/* + * 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_IPMI_MENU_H +#define GUAC_IPMI_MENU_H + +#include + +#include + +/** + * The keysym (Ctrl + ']') which opens the in-terminal IPMI control menu. This + * mirrors the classic Telnet escape character and is unlikely to be needed by + * a serial console. + */ +#define GUAC_IPMI_MENU_KEYSYM 0x5D + +/** + * Opens the in-terminal IPMI control menu, rendering it into the terminal and + * marking the menu as open within the client's data. While open, keystrokes + * should be routed to guac_ipmi_menu_handle_key() rather than forwarded to the + * serial console. + * + * @param client + * The guac_client associated with the IPMI connection. + */ +void guac_ipmi_menu_open(guac_client* client); + +/** + * Handles a keystroke received while the control menu is open, performing the + * selected action (power control, status, identify, etc.) and updating the + * menu state. When a selection completes or the menu is dismissed, the menu is + * closed. + * + * @param client + * The guac_client associated with the IPMI connection. + * + * @param keysym + * The X11 keysym of the pressed key. + */ +void guac_ipmi_menu_handle_key(guac_client* client, int keysym); + +#endif diff --git a/src/protocols/ipmi/pipe.c b/src/protocols/ipmi/pipe.c new file mode 100644 index 0000000000..ce0c46b883 --- /dev/null +++ b/src/protocols/ipmi/pipe.c @@ -0,0 +1,49 @@ +/* + * 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 "ipmi.h" +#include "pipe.h" +#include "terminal/terminal.h" + +#include +#include +#include + +#include + +int guac_ipmi_pipe_handler(guac_user* user, guac_stream* stream, + char* mimetype, char* name) { + + guac_client* client = user->client; + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + + /* Redirect STDIN if pipe has required name */ + if (strcmp(name, GUAC_IPMI_STDIN_PIPE_NAME) == 0) { + guac_terminal_send_stream(ipmi_client->term, user, stream); + return 0; + } + + /* No other inbound pipe streams are supported */ + guac_protocol_send_ack(user->socket, stream, "No such input stream.", + GUAC_PROTOCOL_STATUS_RESOURCE_NOT_FOUND); + guac_socket_flush(user->socket); + return 0; + +} diff --git a/src/protocols/ipmi/pipe.h b/src/protocols/ipmi/pipe.h new file mode 100644 index 0000000000..e09b3383f6 --- /dev/null +++ b/src/protocols/ipmi/pipe.h @@ -0,0 +1,38 @@ +/* + * 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_IPMI_PIPE_H +#define GUAC_IPMI_PIPE_H + +#include + +/** + * The name reserved for the inbound pipe stream which forces the terminal + * emulator's STDIN to be received from the pipe. + */ +#define GUAC_IPMI_STDIN_PIPE_NAME "STDIN" + +/** + * Handles an incoming stream from a Guacamole "pipe" instruction. If the pipe + * is named "STDIN", the contents of the pipe stream are redirected to + * STDIN of the terminal emulator for as long as the pipe is open. + */ +guac_user_pipe_handler guac_ipmi_pipe_handler; + +#endif diff --git a/src/protocols/ipmi/settings.c b/src/protocols/ipmi/settings.c new file mode 100644 index 0000000000..6d83821272 --- /dev/null +++ b/src/protocols/ipmi/settings.c @@ -0,0 +1,589 @@ +/* + * 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 "argv.h" +#include "settings.h" +#include "terminal/terminal.h" + +#include +#include +#include + +#include +#include +#include +#include + +/* Client plugin arguments */ +const char* GUAC_IPMI_CLIENT_ARGS[] = { + "hostname", + "port", + "timeout", + "username", + "password", + "k-g", + "privilege-level", + "cipher-suite", + "sol-payload-instance", + "power-on-connect", + "boot-device", + GUAC_IPMI_ARGV_FONT_NAME, + GUAC_IPMI_ARGV_FONT_SIZE, + GUAC_IPMI_ARGV_COLOR_SCHEME, + "typescript-path", + "typescript-name", + "create-typescript-path", + "typescript-write-existing", + "recording-path", + "recording-name", + "recording-exclude-output", + "recording-exclude-mouse", + "recording-include-keys", + "recording-include-clipboard", + "create-recording-path", + "recording-write-existing", + "read-only", + "backspace", + "terminal-type", + "scrollback", + "disable-copy", + "disable-paste", + NULL +}; + +enum IPMI_ARGS_IDX { + + /** + * The hostname or IP address of the BMC to connect to. Required. + */ + IDX_HOSTNAME, + + /** + * The RMCP port. Informational only; libipmiconsole uses 623/UDP. + */ + IDX_PORT, + + /** + * The IPMI session timeout, in seconds. Optional. + */ + IDX_TIMEOUT, + + /** + * The username to authenticate with. Optional. + */ + IDX_USERNAME, + + /** + * The password to authenticate with. Optional. + */ + IDX_PASSWORD, + + /** + * The BMC key (K_g) for two-key authentication. Optional. + */ + IDX_K_G, + + /** + * The privilege level to authenticate with ("user", "operator", or + * "admin"). Optional; defaults to "admin". + */ + IDX_PRIVILEGE_LEVEL, + + /** + * The RMCP+ cipher suite ID. Optional; defaults to 3. + */ + IDX_CIPHER_SUITE, + + /** + * The SOL payload instance (1-15). Optional; defaults to 1. + */ + IDX_SOL_PAYLOAD_INSTANCE, + + /** + * The chassis power action to perform once connected, prior to attaching + * the console ("none", "on", "reset", or "cycle"). Optional; defaults to + * "none". + */ + IDX_POWER_ON_CONNECT, + + /** + * The boot device override to apply for the next boot ("none", "pxe", + * "disk", "cdrom", or "bios"). Optional; defaults to "none". + */ + IDX_BOOT_DEVICE, + + /** + * The name of the font to use within the terminal. + */ + IDX_FONT_NAME, + + /** + * The size of the font to use within the terminal, in points. + */ + IDX_FONT_SIZE, + + /** + * The color scheme to use, as accepted by the terminal emulator. + */ + IDX_COLOR_SCHEME, + + /** + * The full absolute path to the directory in which typescripts should be + * written. + */ + IDX_TYPESCRIPT_PATH, + + /** + * The name that should be given to typescripts which are written in the + * given path. + */ + IDX_TYPESCRIPT_NAME, + + /** + * Whether the specified typescript path should automatically be created if + * it does not yet exist. + */ + IDX_CREATE_TYPESCRIPT_PATH, + + /** + * Whether existing files should be appended to when creating a new + * typescript. + */ + IDX_TYPESCRIPT_WRITE_EXISTING, + + /** + * The full absolute path to the directory in which screen recordings + * should be written. + */ + IDX_RECORDING_PATH, + + /** + * The name that should be given to screen recordings which are written in + * the given path. + */ + IDX_RECORDING_NAME, + + /** + * Whether broadcast output should NOT be included in the session + * recording. + */ + IDX_RECORDING_EXCLUDE_OUTPUT, + + /** + * Whether mouse state should NOT be included in the session recording. + */ + IDX_RECORDING_EXCLUDE_MOUSE, + + /** + * Whether keys pressed and released should be included in the session + * recording. + */ + IDX_RECORDING_INCLUDE_KEYS, + + /** + * Whether clipboard data should be included in the session recording. + * Clipboard data is NOT included by default within the recording, + * as doing so has privacy and security implications. Including clipboard data + * may be necessary in certain auditing contexts, but should only be done + * with caution. Clipboard data can easily contain sensitive information, such + * as passwords, credit card numbers, etc. + */ + IDX_RECORDING_INCLUDE_CLIPBOARD, + + /** + * Whether the specified screen recording path should automatically be + * created if it does not yet exist. + */ + IDX_CREATE_RECORDING_PATH, + + /** + * Whether existing files should be appended to when creating a new + * recording. + */ + IDX_RECORDING_WRITE_EXISTING, + + /** + * "true" if this connection should be read-only, "false" or blank + * otherwise. + */ + IDX_READ_ONLY, + + /** + * ASCII code, as an integer, to use for the backspace key. + */ + IDX_BACKSPACE, + + /** + * The terminal emulator type that is exposed to the remote system. + */ + IDX_TERMINAL_TYPE, + + /** + * The maximum size of the scrollback buffer in rows. + */ + IDX_SCROLLBACK, + + /** + * Whether outbound clipboard access should be blocked. + */ + IDX_DISABLE_COPY, + + /** + * Whether inbound clipboard access should be blocked. + */ + IDX_DISABLE_PASTE, + + IPMI_ARGS_COUNT +}; + +/** + * Parses the given privilege level string into the corresponding + * guac_ipmi_privilege_level value, logging a warning and falling back to the + * default (admin) for unrecognized values. + * + * @param user + * The user who provided the value, on whose behalf warnings are logged. + * + * @param value + * The privilege level string ("user", "operator", or "admin"). + * + * @return + * The parsed privilege level, or GUAC_IPMI_PRIVILEGE_ADMIN if the value is + * blank or unrecognized. + */ +static guac_ipmi_privilege_level guac_ipmi_parse_privilege_level( + guac_user* user, const char* value) { + + if (value == NULL || strcmp(value, "") == 0 + || strcasecmp(value, "admin") == 0 + || strcasecmp(value, "administrator") == 0) + return GUAC_IPMI_PRIVILEGE_ADMIN; + + if (strcasecmp(value, "operator") == 0) + return GUAC_IPMI_PRIVILEGE_OPERATOR; + + if (strcasecmp(value, "user") == 0) + return GUAC_IPMI_PRIVILEGE_USER; + + guac_user_log(user, GUAC_LOG_WARNING, "Unrecognized privilege level " + "\"%s\"; defaulting to \"admin\".", value); + return GUAC_IPMI_PRIVILEGE_ADMIN; + +} + +/** + * Parses the given power action string into the corresponding + * guac_ipmi_power_action value. Only the actions appropriate for an automatic + * "on connect" action are accepted ("none", "on", "reset", "cycle"). + * + * @param user + * The user who provided the value, on whose behalf warnings are logged. + * + * @param value + * The power action string. + * + * @return + * The parsed power action, or GUAC_IPMI_POWER_NONE if the value is blank + * or unrecognized. + */ +static guac_ipmi_power_action guac_ipmi_parse_power_action( + guac_user* user, const char* value) { + + if (value == NULL || strcmp(value, "") == 0 + || strcasecmp(value, "none") == 0) + return GUAC_IPMI_POWER_NONE; + + if (strcasecmp(value, "on") == 0) + return GUAC_IPMI_POWER_ON; + + if (strcasecmp(value, "reset") == 0) + return GUAC_IPMI_POWER_RESET; + + if (strcasecmp(value, "cycle") == 0) + return GUAC_IPMI_POWER_CYCLE; + + guac_user_log(user, GUAC_LOG_WARNING, "Unrecognized power-on-connect " + "action \"%s\"; no power action will be taken.", value); + return GUAC_IPMI_POWER_NONE; + +} + +/** + * Parses the given boot device string into the corresponding + * guac_ipmi_boot_device value. + * + * @param user + * The user who provided the value, on whose behalf warnings are logged. + * + * @param value + * The boot device string ("none", "pxe", "disk", "cdrom", or "bios"). + * + * @return + * The parsed boot device, or GUAC_IPMI_BOOT_NONE if the value is blank or + * unrecognized. + */ +static guac_ipmi_boot_device guac_ipmi_parse_boot_device( + guac_user* user, const char* value) { + + if (value == NULL || strcmp(value, "") == 0 + || strcasecmp(value, "none") == 0) + return GUAC_IPMI_BOOT_NONE; + + if (strcasecmp(value, "pxe") == 0) + return GUAC_IPMI_BOOT_PXE; + + if (strcasecmp(value, "disk") == 0 || strcasecmp(value, "hd") == 0) + return GUAC_IPMI_BOOT_DISK; + + if (strcasecmp(value, "cdrom") == 0 || strcasecmp(value, "cd") == 0) + return GUAC_IPMI_BOOT_CDROM; + + if (strcasecmp(value, "bios") == 0 || strcasecmp(value, "setup") == 0) + return GUAC_IPMI_BOOT_BIOS; + + guac_user_log(user, GUAC_LOG_WARNING, "Unrecognized boot device " + "\"%s\"; boot order will be left unchanged.", value); + return GUAC_IPMI_BOOT_NONE; + +} + +guac_ipmi_settings* guac_ipmi_parse_args(guac_user* user, + int argc, const char** argv) { + + /* Validate arg count */ + if (argc != IPMI_ARGS_COUNT) { + guac_user_log(user, GUAC_LOG_WARNING, "Incorrect number of connection " + "parameters provided: expected %i, got %i.", + IPMI_ARGS_COUNT, argc); + return NULL; + } + + guac_ipmi_settings* settings = guac_mem_zalloc(sizeof(guac_ipmi_settings)); + + /* Read hostname */ + settings->hostname = + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_HOSTNAME, ""); + + /* Read port (informational only) */ + settings->port = + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_PORT, GUAC_IPMI_DEFAULT_PORT); + + /* Warn if a non-default port was requested, as it cannot be honored */ + if (strcmp(settings->port, GUAC_IPMI_DEFAULT_PORT) != 0) + guac_user_log(user, GUAC_LOG_INFO, "A non-default IPMI port (\"%s\") " + "was specified, but IPMI out-of-band access always uses " + "623/UDP. The provided value will be ignored.", settings->port); + + /* Read IPMI session timeout */ + settings->timeout = + guac_user_parse_args_int(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_TIMEOUT, GUAC_IPMI_DEFAULT_TIMEOUT); + + /* Read credentials */ + settings->username = + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_USERNAME, NULL); + + settings->password = + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_PASSWORD, NULL); + + settings->k_g = + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_K_G, NULL); + + /* Read privilege level */ + settings->privilege_level = guac_ipmi_parse_privilege_level(user, + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_PRIVILEGE_LEVEL, "admin")); + + /* Read cipher suite */ + settings->cipher_suite = + guac_user_parse_args_int(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_CIPHER_SUITE, GUAC_IPMI_DEFAULT_CIPHER_SUITE); + + /* Warn strongly against the use of cipher suite 0 (no security) */ + if (settings->cipher_suite == 0) + guac_user_log(user, GUAC_LOG_WARNING, "Cipher suite 0 provides NO " + "authentication, integrity, or confidentiality. Its use is " + "strongly discouraged."); + + /* Read SOL payload instance */ + settings->sol_payload_instance = + guac_user_parse_args_int(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_SOL_PAYLOAD_INSTANCE, GUAC_IPMI_DEFAULT_SOL_PAYLOAD_INSTANCE); + + /* Read pre-connect power and boot actions */ + settings->power_on_connect = guac_ipmi_parse_power_action(user, + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_POWER_ON_CONNECT, "none")); + + settings->boot_device = guac_ipmi_parse_boot_device(user, + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_BOOT_DEVICE, "none")); + + /* Read-only mode */ + settings->read_only = + guac_user_parse_args_boolean(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_READ_ONLY, false); + + /* Read maximum scrollback size */ + settings->max_scrollback = + guac_user_parse_args_int(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_SCROLLBACK, GUAC_TERMINAL_DEFAULT_MAX_SCROLLBACK); + + /* Read font name */ + settings->font_name = + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_FONT_NAME, GUAC_TERMINAL_DEFAULT_FONT_NAME); + + /* Read font size */ + settings->font_size = + guac_user_parse_args_int(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_FONT_SIZE, GUAC_TERMINAL_DEFAULT_FONT_SIZE); + + /* Copy requested color scheme */ + settings->color_scheme = + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_COLOR_SCHEME, GUAC_TERMINAL_DEFAULT_COLOR_SCHEME); + + /* Pull width/height/resolution directly from user */ + settings->width = user->info.optimal_width; + settings->height = user->info.optimal_height; + settings->resolution = user->info.optimal_resolution; + + /* Read typescript path */ + settings->typescript_path = + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_TYPESCRIPT_PATH, NULL); + + /* Read typescript name */ + settings->typescript_name = + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_TYPESCRIPT_NAME, GUAC_IPMI_DEFAULT_TYPESCRIPT_NAME); + + /* Parse path creation flag */ + settings->create_typescript_path = + guac_user_parse_args_boolean(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_CREATE_TYPESCRIPT_PATH, false); + + /* Parse allow write existing file flag */ + settings->typescript_write_existing = + guac_user_parse_args_boolean(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_TYPESCRIPT_WRITE_EXISTING, false); + + /* Read recording path */ + settings->recording_path = + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_RECORDING_PATH, NULL); + + /* Read recording name */ + settings->recording_name = + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_RECORDING_NAME, GUAC_IPMI_DEFAULT_RECORDING_NAME); + + /* Parse output exclusion flag */ + settings->recording_exclude_output = + guac_user_parse_args_boolean(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_RECORDING_EXCLUDE_OUTPUT, false); + + /* Parse mouse exclusion flag */ + settings->recording_exclude_mouse = + guac_user_parse_args_boolean(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_RECORDING_EXCLUDE_MOUSE, false); + + /* Parse key event inclusion flag */ + settings->recording_include_keys = + guac_user_parse_args_boolean(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_RECORDING_INCLUDE_KEYS, false); + + /* Parse clipboard inclusion flag */ + settings->recording_include_clipboard = + guac_user_parse_args_boolean(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_RECORDING_INCLUDE_CLIPBOARD, false); + + /* Parse path creation flag */ + settings->create_recording_path = + guac_user_parse_args_boolean(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_CREATE_RECORDING_PATH, false); + + /* Parse allow write existing file flag */ + settings->recording_write_existing = + guac_user_parse_args_boolean(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_RECORDING_WRITE_EXISTING, false); + + /* Parse backspace key code */ + settings->backspace = + guac_user_parse_args_int(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_BACKSPACE, GUAC_TERMINAL_DEFAULT_BACKSPACE); + + /* Read terminal emulator type */ + settings->terminal_type = + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_TERMINAL_TYPE, "linux"); + + /* Parse clipboard copy disable flag */ + settings->disable_copy = + guac_user_parse_args_boolean(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_DISABLE_COPY, false); + + /* Parse clipboard paste disable flag */ + settings->disable_paste = + guac_user_parse_args_boolean(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_DISABLE_PASTE, false); + + /* Parsing was successful */ + return settings; + +} + +void guac_ipmi_settings_free(guac_ipmi_settings* settings) { + + /* Free network connection information */ + guac_mem_free(settings->hostname); + guac_mem_free(settings->port); + + /* Free credentials */ + guac_mem_free(settings->username); + guac_mem_free(settings->password); + guac_mem_free(settings->k_g); + + /* Free display preferences */ + guac_mem_free(settings->font_name); + guac_mem_free(settings->color_scheme); + + /* Free typescript settings */ + guac_mem_free(settings->typescript_name); + guac_mem_free(settings->typescript_path); + + /* Free screen recording settings */ + guac_mem_free(settings->recording_name); + guac_mem_free(settings->recording_path); + + /* Free terminal emulator type */ + guac_mem_free(settings->terminal_type); + + /* Free overall structure */ + guac_mem_free(settings); + +} diff --git a/src/protocols/ipmi/settings.h b/src/protocols/ipmi/settings.h new file mode 100644 index 0000000000..89a2ba13a5 --- /dev/null +++ b/src/protocols/ipmi/settings.h @@ -0,0 +1,396 @@ +/* + * 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_IPMI_SETTINGS_H +#define GUAC_IPMI_SETTINGS_H + +#include + +#include +#include + +/** + * The RMCP port used for IPMI 2.0 out-of-band access. This is fixed at 623/UDP + * by libipmiconsole and libfreeipmi; the "port" parameter is accepted for + * forward compatibility and informational logging only. + */ +#define GUAC_IPMI_DEFAULT_PORT "623" + +/** + * 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_IPMI_PROCESS_TITLE_NAME "ipmi" + +/** + * The default IPMI session timeout, in seconds, used when establishing the + * Serial-over-LAN session. + */ +#define GUAC_IPMI_DEFAULT_TIMEOUT 60 + +/** + * The default RMCP+ cipher suite ID. Suite 3 (HMAC-SHA1 / HMAC-SHA1-96 / + * AES-CBC-128) is the most widely supported authenticated and encrypted + * suite. Cipher suite 0 (no auth/integrity/confidentiality) is intentionally + * NOT the default for security reasons. + */ +#define GUAC_IPMI_DEFAULT_CIPHER_SUITE 3 + +/** + * The default SOL payload instance. Most BMCs support only a single instance. + */ +#define GUAC_IPMI_DEFAULT_SOL_PAYLOAD_INSTANCE 1 + +/** + * The filename to use for the typescript, if not specified. + */ +#define GUAC_IPMI_DEFAULT_TYPESCRIPT_NAME "typescript" + +/** + * The filename to use for the screen recording, if not specified. + */ +#define GUAC_IPMI_DEFAULT_RECORDING_NAME "recording" + +/** + * The privilege level to authenticate with, expressed using the + * IPMICONSOLE_PRIVILEGE_* constants from libipmiconsole. The values here + * intentionally match those constants. + */ +typedef enum guac_ipmi_privilege_level { + GUAC_IPMI_PRIVILEGE_USER = 0, + GUAC_IPMI_PRIVILEGE_OPERATOR = 1, + GUAC_IPMI_PRIVILEGE_ADMIN = 2 +} guac_ipmi_privilege_level; + +/** + * A chassis power action which may be performed against the BMC, either + * automatically when the connection is established or on demand via the + * in-terminal control menu. + */ +typedef enum guac_ipmi_power_action { + + /** + * No power action. + */ + GUAC_IPMI_POWER_NONE, + + /** + * Power the system on (IPMI_CHASSIS_CONTROL_POWER_UP). + */ + GUAC_IPMI_POWER_ON, + + /** + * Hard power the system off (IPMI_CHASSIS_CONTROL_POWER_DOWN). + */ + GUAC_IPMI_POWER_OFF, + + /** + * Power cycle the system (IPMI_CHASSIS_CONTROL_POWER_CYCLE). + */ + GUAC_IPMI_POWER_CYCLE, + + /** + * Hard reset the system (IPMI_CHASSIS_CONTROL_HARD_RESET). + */ + GUAC_IPMI_POWER_RESET, + + /** + * Request a graceful (ACPI) shutdown + * (IPMI_CHASSIS_CONTROL_INITIATE_SOFT_SHUTDOWN). + */ + GUAC_IPMI_POWER_SOFT_SHUTDOWN, + + /** + * Pulse a diagnostic interrupt / NMI + * (IPMI_CHASSIS_CONTROL_PULSE_DIAGNOSTIC_INTERRUPT). + */ + GUAC_IPMI_POWER_DIAGNOSTIC_INTERRUPT + +} guac_ipmi_power_action; + +/** + * A boot device override which may be applied for the next system boot. + */ +typedef enum guac_ipmi_boot_device { + + /** + * No boot device override (leave the BMC's configured boot order). + */ + GUAC_IPMI_BOOT_NONE, + + /** + * Force the next boot from the network (PXE). + */ + GUAC_IPMI_BOOT_PXE, + + /** + * Force the next boot from the primary hard drive. + */ + GUAC_IPMI_BOOT_DISK, + + /** + * Force the next boot from removable media (CD/DVD). + */ + GUAC_IPMI_BOOT_CDROM, + + /** + * Force the next boot into the BIOS/UEFI setup utility. + */ + GUAC_IPMI_BOOT_BIOS + +} guac_ipmi_boot_device; + +/** + * Settings for the IPMI Serial-over-LAN connection. The values for this + * structure are parsed from the arguments given during the Guacamole protocol + * handshake using the guac_ipmi_parse_args() function. + */ +typedef struct guac_ipmi_settings { + + /** + * The hostname or IP address of the BMC to connect to. + */ + char* hostname; + + /** + * The RMCP port, as a string. Accepted for forward compatibility only; + * libipmiconsole always uses 623/UDP. + */ + char* port; + + /** + * The IPMI session timeout, in seconds. + */ + int timeout; + + /** + * The username to authenticate with, if any. NULL if unspecified (the BMC + * default / null username is used). + */ + char* username; + + /** + * The password to authenticate with, if any. NULL if unspecified. + */ + char* password; + + /** + * The BMC key (K_g) for two-key authentication, if any. NULL if + * unspecified, in which case the password is used as the BMC key. + */ + char* k_g; + + /** + * The privilege level to authenticate with. + */ + guac_ipmi_privilege_level privilege_level; + + /** + * The RMCP+ cipher suite ID determining the authentication, integrity, and + * confidentiality algorithms used. + */ + int cipher_suite; + + /** + * The SOL payload instance to use (1-15). + */ + int sol_payload_instance; + + /** + * The chassis power action to perform automatically once the connection is + * established, prior to attaching the console. GUAC_IPMI_POWER_NONE if no + * action should be taken. + */ + guac_ipmi_power_action power_on_connect; + + /** + * The boot device override to apply (for the next boot) prior to any + * power-on-connect action. GUAC_IPMI_BOOT_NONE if the BMC's configured + * boot order should be left unchanged. + */ + guac_ipmi_boot_device boot_device; + + /** + * Whether this connection is read-only, and user input should be dropped. + */ + bool read_only; + + /** + * The maximum size of the scrollback buffer in rows. + */ + int max_scrollback; + + /** + * The name of the font to use for display rendering. + */ + char* font_name; + + /** + * The size of the font to use, in points. + */ + int font_size; + + /** + * The name of the color scheme to use. + */ + char* color_scheme; + + /** + * The desired width of the terminal display, in pixels. + */ + int width; + + /** + * The desired height of the terminal display, in pixels. + */ + int height; + + /** + * The desired screen resolution, in DPI. + */ + int resolution; + + /** + * Whether outbound clipboard access should be blocked. + */ + bool disable_copy; + + /** + * Whether inbound clipboard access should be blocked. + */ + bool disable_paste; + + /** + * The path in which the typescript should be saved, if enabled. NULL if no + * typescript should be saved. + */ + char* typescript_path; + + /** + * The filename to use for the typescript, if enabled. + */ + char* typescript_name; + + /** + * Whether the typescript path should be automatically created if it does + * not already exist. + */ + bool create_typescript_path; + + /** + * Whether existing files should be appended to when creating a new + * typescript. + */ + bool typescript_write_existing; + + /** + * The path in which the screen recording should be saved, if enabled. NULL + * if no screen recording should be saved. + */ + char* recording_path; + + /** + * The filename to use for the screen recording, if enabled. + */ + char* recording_name; + + /** + * Whether the screen recording path should be automatically created if it + * does not already exist. + */ + bool create_recording_path; + + /** + * Whether output which is broadcast to each connected client should NOT be + * included in the session recording. + */ + bool recording_exclude_output; + + /** + * Whether changes to mouse state should NOT be included in the session + * recording. + */ + bool recording_exclude_mouse; + + /** + * Whether keys pressed and released should be included in the session + * recording. + */ + bool recording_include_keys; + + /** + * Whether clipboard data should be included in the session recording. + */ + bool recording_include_clipboard; + + /** + * Whether existing files should be appended to when creating a new + * recording. + */ + bool recording_write_existing; + + /** + * The ASCII code, as an integer, that the terminal will send when the + * backspace key is pressed. + */ + int backspace; + + /** + * The terminal emulator type that is exposed to the remote system. + */ + char* terminal_type; + +} guac_ipmi_settings; + +/** + * Parses all given args, storing them in a newly-allocated settings object. If + * the args fail to parse, NULL is returned. + * + * @param user + * The user who submitted the given arguments while joining the connection. + * + * @param argc + * The number of arguments within the argv array. + * + * @param argv + * The values of all arguments provided by the user. + * + * @return + * A newly-allocated settings object which must be freed with + * guac_ipmi_settings_free() when no longer needed. If the arguments fail + * to parse, NULL is returned. + */ +guac_ipmi_settings* guac_ipmi_parse_args(guac_user* user, + int argc, const char** argv); + +/** + * Frees the given guac_ipmi_settings object, having been previously allocated + * via guac_ipmi_parse_args(). + * + * @param settings + * The settings object to free. + */ +void guac_ipmi_settings_free(guac_ipmi_settings* settings); + +/** + * NULL-terminated array of accepted client args. + */ +extern const char* GUAC_IPMI_CLIENT_ARGS[]; + +#endif diff --git a/src/protocols/ipmi/user.c b/src/protocols/ipmi/user.c new file mode 100644 index 0000000000..47e0a562ed --- /dev/null +++ b/src/protocols/ipmi/user.c @@ -0,0 +1,115 @@ +/* + * 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 "argv.h" +#include "clipboard.h" +#include "input.h" +#include "ipmi.h" +#include "pipe.h" +#include "settings.h" +#include "terminal/terminal.h" +#include "user.h" + +#include +#include +#include +#include + +#include +#include + +int guac_ipmi_user_join_handler(guac_user* user, int argc, char** argv) { + + guac_client* client = user->client; + guac_ipmi_client* ipmi_client = (guac_ipmi_client*) client->data; + + /* Parse provided arguments */ + guac_ipmi_settings* settings = guac_ipmi_parse_args(user, + argc, (const char**) argv); + + /* Fail if settings cannot be parsed */ + if (settings == NULL) { + guac_user_log(user, GUAC_LOG_INFO, + "Badly formatted client arguments."); + return 1; + } + + /* Store settings at user level */ + user->data = settings; + + /* Connect via IPMI if owner */ + if (user->owner) { + + /* Store owner's settings at client level */ + ipmi_client->settings = settings; + + /* Start client thread */ + if (pthread_create(&(ipmi_client->client_thread), NULL, + guac_ipmi_client_thread, (void*) client)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Unable to start IPMI client thread"); + return 1; + } + + } + + /* Only handle events if not read-only */ + if (!settings->read_only) { + + /* General mouse/keyboard events */ + user->key_handler = guac_ipmi_user_key_handler; + user->mouse_handler = guac_ipmi_user_mouse_handler; + + /* Inbound (client to server) clipboard transfer */ + if (!settings->disable_paste) + user->clipboard_handler = guac_ipmi_clipboard_handler; + + /* STDIN redirection */ + user->pipe_handler = guac_ipmi_pipe_handler; + + /* Updates to connection parameters */ + user->argv_handler = guac_argv_handler; + + /* Display size change events */ + user->size_handler = guac_ipmi_user_size_handler; + + } + + return 0; + +} + +int guac_ipmi_user_leave_handler(guac_user* user) { + + guac_ipmi_client* ipmi_client = + (guac_ipmi_client*) user->client->data; + + /* Remove the user from the terminal */ + guac_terminal_remove_user(ipmi_client->term, user); + + /* Free settings if not owner (owner settings will be freed with client) */ + if (!user->owner) { + guac_ipmi_settings* settings = (guac_ipmi_settings*) user->data; + guac_ipmi_settings_free(settings); + } + + return 0; +} diff --git a/src/protocols/ipmi/user.h b/src/protocols/ipmi/user.h new file mode 100644 index 0000000000..5b35a56ae6 --- /dev/null +++ b/src/protocols/ipmi/user.h @@ -0,0 +1,35 @@ +/* + * 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_IPMI_USER_H +#define GUAC_IPMI_USER_H + +#include + +/** + * Handler for joining users. + */ +guac_user_join_handler guac_ipmi_user_join_handler; + +/** + * Handler for leaving users. + */ +guac_user_leave_handler guac_ipmi_user_leave_handler; + +#endif From 49e05b0e1f4307cb74c01ef57830136b73bb90f3 Mon Sep 17 00:00:00 2001 From: Ciro Iriarte Date: Fri, 10 Jul 2026 15:38:50 -0300 Subject: [PATCH 02/14] GUACAMOLE-2300: IPMI: fix module linking under --as-needed linkers Move @IPMI_LIBS@ (-lipmiconsole -lfreeipmi) from _LDFLAGS to _LIBADD so the libraries follow the object files on the link line. With the GNU linker's --as-needed behavior (default on openSUSE, and modern Debian/Ubuntu/Fedora), libraries listed in LDFLAGS precede the objects that reference them and are dropped, leaving them out of DT_NEEDED. The .so still builds, but guacd's dlopen() then fails at runtime with 'undefined symbol: tmpl_cmd_set_system_boot_options_rs' and reports the protocol as not installed. --- src/protocols/ipmi/Makefile.am | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/protocols/ipmi/Makefile.am b/src/protocols/ipmi/Makefile.am index 1f654a917b..386067e6c6 100644 --- a/src/protocols/ipmi/Makefile.am +++ b/src/protocols/ipmi/Makefile.am @@ -61,9 +61,9 @@ libguac_client_ipmi_la_CFLAGS = \ libguac_client_ipmi_la_LIBADD = \ @COMMON_LTLIB@ \ @LIBGUAC_LTLIB@ \ - @TERMINAL_LTLIB@ + @TERMINAL_LTLIB@ \ + @IPMI_LIBS@ libguac_client_ipmi_la_LDFLAGS = \ -version-info 0:0:0 \ - @PTHREAD_LIBS@ \ - @IPMI_LIBS@ + @PTHREAD_LIBS@ From 991f6a98419df80ffc07d03f2ca3203f17b29fd7 Mon Sep 17 00:00:00 2001 From: Ciro Iriarte Date: Fri, 10 Jul 2026 15:39:10 -0300 Subject: [PATCH 03/14] GUACAMOLE-2300: IPMI: multivendor workarounds, encryption policy, engine refcount, hex K_g M1 hardening for real-world multivendor BMC support: - workaround-flags parameter: comma-separated FreeIPMI workaround tokens and vendor presets (supermicro/intel/sun/dell/hpe/lenovo), resolved into separate libipmiconsole (SOL) and libfreeipmi (chassis) masks and applied to both sessions, replacing the hardcoded 0. - encryption-policy parameter (required default / preferred / none): validates that the configured cipher suite provides confidentiality before connecting; aborts under 'required' for non-encrypting suites (0,1,2,6,7,8,11,15). - Reference-counted, mutex-guarded libipmiconsole engine init/teardown so concurrent IPMI connections no longer tear down the shared engine out from under each other. - K_g now accepts a 0x-prefixed hex value decoded to a raw byte buffer with an explicit length, so binary BMC keys (which may contain NUL bytes) are no longer truncated by strlen(). --- src/protocols/ipmi/chassis.c | 8 +- src/protocols/ipmi/client.c | 2 +- src/protocols/ipmi/ipmi.c | 79 ++++++++- src/protocols/ipmi/ipmi.h | 18 +++ src/protocols/ipmi/settings.c | 294 +++++++++++++++++++++++++++++++++- src/protocols/ipmi/settings.h | 69 +++++++- 6 files changed, 458 insertions(+), 12 deletions(-) diff --git a/src/protocols/ipmi/chassis.c b/src/protocols/ipmi/chassis.c index 6fca58d56f..b671b3cf17 100644 --- a/src/protocols/ipmi/chassis.c +++ b/src/protocols/ipmi/chassis.c @@ -155,10 +155,10 @@ static ipmi_ctx_t guac_ipmi_chassis_connect(guac_client* client) { return NULL; } - /* The BMC key (K_g) is optional; its length is required separately as it - * may legitimately contain null bytes. */ + /* The BMC key (K_g) is optional; its length is stored separately as it may + * legitimately contain null bytes. */ const unsigned char* k_g = (const unsigned char*) settings->k_g; - unsigned int k_g_len = settings->k_g != NULL ? strlen(settings->k_g) : 0; + unsigned int k_g_len = settings->k_g_length; /* Open an IPMI 2.0 (RMCP+) out-of-band session. Session and retransmission * timeouts of 0 request the library defaults. */ @@ -171,7 +171,7 @@ static ipmi_ctx_t guac_ipmi_chassis_connect(guac_client* client) { (uint8_t) settings->cipher_suite, GUAC_IPMI_CHASSIS_SESSION_TIMEOUT, GUAC_IPMI_CHASSIS_RETRANSMISSION_TIMEOUT, - 0 /* workaround flags */, + settings->chassis_workaround_flags, 0 /* flags */) < 0) { guac_client_log(client, GUAC_LOG_ERROR, "Unable to open IPMI session for chassis command: %s", diff --git a/src/protocols/ipmi/client.c b/src/protocols/ipmi/client.c index e936c03b9b..5c03b9dcbe 100644 --- a/src/protocols/ipmi/client.c +++ b/src/protocols/ipmi/client.c @@ -126,7 +126,7 @@ int guac_ipmi_client_free_handler(guac_client* client) { if (ipmi_client->console_ctx != NULL) { pthread_join(ipmi_client->client_thread, NULL); ipmiconsole_ctx_destroy(ipmi_client->console_ctx); - ipmiconsole_engine_teardown(0); + guac_ipmi_engine_unref(); } /* Free settings */ diff --git a/src/protocols/ipmi/ipmi.c b/src/protocols/ipmi/ipmi.c index 92d2ccdb34..80fb91f55f 100644 --- a/src/protocols/ipmi/ipmi.c +++ b/src/protocols/ipmi/ipmi.c @@ -41,6 +41,53 @@ #include #include +/** + * Lock guarding the process-global libipmiconsole engine reference count. The + * libipmiconsole engine is a single process-wide resource; initializing or + * tearing it down per connection would race with, and disrupt, any other IPMI + * connections active within the same guacd process. + */ +static pthread_mutex_t guac_ipmi_engine_lock = PTHREAD_MUTEX_INITIALIZER; + +/** + * The number of IPMI connections currently holding the libipmiconsole engine. + * The engine is initialized when this count rises from zero and torn down when + * it returns to zero. + */ +static int guac_ipmi_engine_refcount = 0; + +int guac_ipmi_engine_ref() { + + int result = 0; + + pthread_mutex_lock(&guac_ipmi_engine_lock); + + /* Initialize the shared engine only on the first reference */ + if (guac_ipmi_engine_refcount == 0) + result = ipmiconsole_engine_init(0, 0); + + /* Take the reference only if the engine is available */ + if (result == 0) + guac_ipmi_engine_refcount++; + + pthread_mutex_unlock(&guac_ipmi_engine_lock); + + return result; + +} + +void guac_ipmi_engine_unref() { + + pthread_mutex_lock(&guac_ipmi_engine_lock); + + /* Tear down the shared engine only once the last reference is released */ + if (guac_ipmi_engine_refcount > 0 && --guac_ipmi_engine_refcount == 0) + ipmiconsole_engine_teardown(0); + + pthread_mutex_unlock(&guac_ipmi_engine_lock); + +} + /** * Write the entire buffer given to the specified file descriptor, retrying the * write automatically if necessary. @@ -167,10 +214,10 @@ static ipmiconsole_ctx_t __guac_ipmi_create_session(guac_client* client) { .username = settings->username, .password = settings->password, .k_g = (unsigned char*) settings->k_g, - .k_g_len = settings->k_g != NULL ? strlen(settings->k_g) : 0, + .k_g_len = settings->k_g_length, .privilege_level = settings->privilege_level, .cipher_suite_id = settings->cipher_suite, - .workaround_flags = 0 + .workaround_flags = settings->sol_workaround_flags }; struct ipmiconsole_protocol_config protocol_config = { @@ -322,8 +369,30 @@ void* guac_ipmi_client_thread(void* data) { settings->typescript_write_existing); } - /* Initialize the libipmiconsole engine */ - if (ipmiconsole_engine_init(0, 0) < 0) { + /* Enforce the configured encryption policy before establishing any session + * with the BMC */ + if (!guac_ipmi_cipher_provides_confidentiality(settings->cipher_suite)) { + + if (settings->encryption_policy == GUAC_IPMI_ENCRYPTION_REQUIRED) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, + "Cipher suite %i does not provide confidentiality " + "(payload encryption), but the encryption policy requires " + "it. Refusing to connect. Select an encrypting cipher " + "suite (e.g. 3 or 17) or relax the encryption policy.", + settings->cipher_suite); + return NULL; + } + + if (settings->encryption_policy == GUAC_IPMI_ENCRYPTION_PREFERRED) + guac_client_log(client, GUAC_LOG_WARNING, "Cipher suite %i does " + "not provide confidentiality; the SOL session and " + "credentials will NOT be encrypted on the wire.", + settings->cipher_suite); + + } + + /* Initialize (or reference) the shared libipmiconsole engine */ + if (guac_ipmi_engine_ref() < 0) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to initialize IPMI console engine."); return NULL; @@ -333,7 +402,7 @@ void* guac_ipmi_client_thread(void* data) { ipmi_client->console_ctx = __guac_ipmi_create_session(client); if (ipmi_client->console_ctx == NULL) { /* Already aborted within __guac_ipmi_create_session() */ - ipmiconsole_engine_teardown(0); + guac_ipmi_engine_unref(); return NULL; } diff --git a/src/protocols/ipmi/ipmi.h b/src/protocols/ipmi/ipmi.h index 9df9e79af0..72fa4b86ed 100644 --- a/src/protocols/ipmi/ipmi.h +++ b/src/protocols/ipmi/ipmi.h @@ -96,4 +96,22 @@ typedef struct guac_ipmi_client { */ void* guac_ipmi_client_thread(void* data); +/** + * Acquires a reference to the process-global libipmiconsole engine, + * initializing it if this is the first reference. Each successful call must be + * balanced by a later call to guac_ipmi_engine_unref(). This is thread-safe. + * + * @return + * Zero on success, or negative if the engine could not be initialized (in + * which case no reference is held). + */ +int guac_ipmi_engine_ref(); + +/** + * Releases a reference to the process-global libipmiconsole engine, tearing it + * down once the final reference is released. Must only be called to balance a + * prior successful guac_ipmi_engine_ref(). This is thread-safe. + */ +void guac_ipmi_engine_unref(); + #endif diff --git a/src/protocols/ipmi/settings.c b/src/protocols/ipmi/settings.c index 6d83821272..8ae16d2303 100644 --- a/src/protocols/ipmi/settings.c +++ b/src/protocols/ipmi/settings.c @@ -27,7 +27,11 @@ #include #include +#include +#include + #include +#include #include #include #include @@ -42,6 +46,8 @@ const char* GUAC_IPMI_CLIENT_ARGS[] = { "k-g", "privilege-level", "cipher-suite", + "encryption-policy", + "workaround-flags", "sol-payload-instance", "power-on-connect", "boot-device", @@ -112,6 +118,20 @@ enum IPMI_ARGS_IDX { */ IDX_CIPHER_SUITE, + /** + * The encryption policy governing whether a non-confidential cipher suite + * is permitted ("required", "preferred", or "none"). Optional; defaults to + * "required". + */ + IDX_ENCRYPTION_POLICY, + + /** + * A comma-separated list of FreeIPMI workaround flags and/or vendor preset + * names to apply to the SOL and chassis sessions. Optional; defaults to + * none. + */ + IDX_WORKAROUND_FLAGS, + /** * The SOL payload instance (1-15). Optional; defaults to 1. */ @@ -365,6 +385,259 @@ static guac_ipmi_boot_device guac_ipmi_parse_boot_device( } +/** + * Parses the given string into a guac_ipmi_encryption_policy. Unrecognized + * values fall back to the (secure) default of "required". + */ +static guac_ipmi_encryption_policy guac_ipmi_parse_encryption_policy( + guac_user* user, const char* value) { + + if (value == NULL || strcmp(value, "") == 0 + || strcasecmp(value, "required") == 0) + return GUAC_IPMI_ENCRYPTION_REQUIRED; + + if (strcasecmp(value, "preferred") == 0) + return GUAC_IPMI_ENCRYPTION_PREFERRED; + + if (strcasecmp(value, "none") == 0) + return GUAC_IPMI_ENCRYPTION_NONE; + + guac_user_log(user, GUAC_LOG_WARNING, "Unrecognized encryption policy " + "\"%s\"; requiring encryption.", value); + return GUAC_IPMI_ENCRYPTION_REQUIRED; + +} + +int guac_ipmi_cipher_provides_confidentiality(int cipher_suite) { + + /* IPMI 2.0 RMCP+ cipher suites whose confidentiality algorithm is NOT + * "none" (i.e. the payload is encrypted). Suites 0, 1, 2, 6, 7, 11, 15, + * and 16 provide no confidentiality and are therefore excluded. */ + switch (cipher_suite) { + case 3: case 4: case 5: + case 8: case 9: case 10: + case 12: case 13: case 14: + case 17: case 18: case 19: + return 1; + default: + return 0; + } + +} + +/** + * A single FreeIPMI workaround flag, mapping a user-facing token to the + * corresponding libipmiconsole (SOL) and libfreeipmi (chassis) flag bits. A + * chassis bit of 0 indicates the flag has no equivalent in the chassis + * (out-of-band 2.0) namespace and applies to the SOL session only. + */ +typedef struct guac_ipmi_workaround_token { + const char* name; + unsigned int sol_flag; + unsigned int chassis_flag; +} guac_ipmi_workaround_token; + +static const guac_ipmi_workaround_token GUAC_IPMI_WORKAROUND_TOKENS[] = { + { "authcap", IPMICONSOLE_WORKAROUND_AUTHENTICATION_CAPABILITIES, IPMI_WORKAROUND_FLAGS_OUTOFBAND_2_0_AUTHENTICATION_CAPABILITIES }, + { "intel20", IPMICONSOLE_WORKAROUND_INTEL_2_0_SESSION, IPMI_WORKAROUND_FLAGS_OUTOFBAND_2_0_INTEL_2_0_SESSION }, + { "supermicro20", IPMICONSOLE_WORKAROUND_SUPERMICRO_2_0_SESSION, IPMI_WORKAROUND_FLAGS_OUTOFBAND_2_0_SUPERMICRO_2_0_SESSION }, + { "sun20", IPMICONSOLE_WORKAROUND_SUN_2_0_SESSION, IPMI_WORKAROUND_FLAGS_OUTOFBAND_2_0_SUN_2_0_SESSION }, + { "opensesspriv", IPMICONSOLE_WORKAROUND_OPEN_SESSION_PRIVILEGE, IPMI_WORKAROUND_FLAGS_OUTOFBAND_2_0_OPEN_SESSION_PRIVILEGE }, + { "integritycheckvalue", IPMICONSOLE_WORKAROUND_NON_EMPTY_INTEGRITY_CHECK_VALUE, IPMI_WORKAROUND_FLAGS_OUTOFBAND_2_0_NON_EMPTY_INTEGRITY_CHECK_VALUE }, + { "nochecksumcheck", IPMICONSOLE_WORKAROUND_NO_CHECKSUM_CHECK, IPMI_WORKAROUND_FLAGS_OUTOFBAND_2_0_NO_CHECKSUM_CHECK }, + { "serialalertsdeferred", IPMICONSOLE_WORKAROUND_SERIAL_ALERTS_DEFERRED, 0 }, + { "solpacketseq", IPMICONSOLE_WORKAROUND_INCREMENT_SOL_PACKET_SEQUENCE, 0 }, + { "solpayloadsize", IPMICONSOLE_WORKAROUND_IGNORE_SOL_PAYLOAD_SIZE, 0 }, + { "solport", IPMICONSOLE_WORKAROUND_IGNORE_SOL_PORT, 0 }, + { "solstatus", IPMICONSOLE_WORKAROUND_SKIP_SOL_ACTIVATION_STATUS, 0 }, + { "channelpayload", IPMICONSOLE_WORKAROUND_SKIP_CHANNEL_PAYLOAD_SUPPORT, 0 } +}; + +/** + * A vendor preset expanding to a set of individual workaround tokens known to + * be commonly required for that vendor's BMCs. + */ +typedef struct guac_ipmi_workaround_preset { + const char* name; + const char* tokens; +} guac_ipmi_workaround_preset; + +static const guac_ipmi_workaround_preset GUAC_IPMI_WORKAROUND_PRESETS[] = { + { "none", "" }, + { "supermicro", "supermicro20,opensesspriv,integritycheckvalue,solpayloadsize,solport,solstatus" }, + { "intel", "intel20,opensesspriv,integritycheckvalue" }, + { "sun", "sun20,authcap,opensesspriv,solpayloadsize" }, + { "dell", "opensesspriv,solpayloadsize" }, + + /* Modern HPE iLO and Lenovo XCC generations generally require no + * workarounds; these presets are provided for discoverability and may be + * combined with explicit tokens for older firmware. */ + { "hpe", "" }, + { "lenovo", "" } +}; + +/** + * Applies a single individual workaround flag token (not a preset) to the + * given SOL and chassis masks. Returns non-zero if the token was recognized. + */ +static int guac_ipmi_apply_workaround_token(const char* token, + unsigned int* sol, unsigned int* chassis) { + + int count = sizeof(GUAC_IPMI_WORKAROUND_TOKENS) + / sizeof(GUAC_IPMI_WORKAROUND_TOKENS[0]); + + for (int i = 0; i < count; i++) { + if (strcasecmp(token, GUAC_IPMI_WORKAROUND_TOKENS[i].name) == 0) { + *sol |= GUAC_IPMI_WORKAROUND_TOKENS[i].sol_flag; + *chassis |= GUAC_IPMI_WORKAROUND_TOKENS[i].chassis_flag; + return 1; + } + } + + return 0; + +} + +/** + * Parses a comma-separated list of workaround flag tokens and/or vendor preset + * names, accumulating the resulting flags into the given SOL (libipmiconsole) + * and chassis (libfreeipmi) masks. Both masks are cleared to 0 first. + */ +static void guac_ipmi_parse_workaround_flags(guac_user* user, + const char* value, unsigned int* sol, unsigned int* chassis) { + + *sol = 0; + *chassis = 0; + + if (value == NULL || strcmp(value, "") == 0) + return; + + int preset_count = sizeof(GUAC_IPMI_WORKAROUND_PRESETS) + / sizeof(GUAC_IPMI_WORKAROUND_PRESETS[0]); + + /* Tokenize a mutable copy of the value on commas */ + char* copy = guac_strdup(value); + char* saveptr = NULL; + char* token = strtok_r(copy, ",", &saveptr); + + while (token != NULL) { + + /* Trim leading whitespace */ + while (*token == ' ' || *token == '\t') + token++; + + /* Trim trailing whitespace */ + char* end = token + strlen(token); + while (end > token && (end[-1] == ' ' || end[-1] == '\t')) + *(--end) = '\0'; + + if (*token != '\0') { + + /* Check for a vendor preset first */ + int matched = 0; + for (int i = 0; i < preset_count; i++) { + if (strcasecmp(token, GUAC_IPMI_WORKAROUND_PRESETS[i].name) == 0) { + + /* Expand the preset's individual tokens */ + char* preset_copy = + guac_strdup(GUAC_IPMI_WORKAROUND_PRESETS[i].tokens); + char* preset_save = NULL; + char* sub = strtok_r(preset_copy, ",", &preset_save); + while (sub != NULL) { + guac_ipmi_apply_workaround_token(sub, sol, chassis); + sub = strtok_r(NULL, ",", &preset_save); + } + guac_mem_free(preset_copy); + + matched = 1; + break; + } + } + + /* Otherwise treat as an individual flag token */ + if (!matched && !guac_ipmi_apply_workaround_token(token, sol, chassis)) + guac_user_log(user, GUAC_LOG_WARNING, "Unrecognized IPMI " + "workaround flag \"%s\"; ignoring.", token); + + } + + token = strtok_r(NULL, ",", &saveptr); + } + + guac_mem_free(copy); + +} + +/** + * The maximum length of an IPMI 2.0 K_g (BMC) key, in bytes. + */ +#define GUAC_IPMI_K_G_MAX_LENGTH 20 + +/** + * Parses the raw K_g argument into a newly-allocated byte buffer, returning + * the buffer and storing its length in *length. If the value begins with "0x", + * it is decoded as a hexadecimal byte string (allowing binary keys that may + * contain NUL bytes); otherwise it is treated as a raw passphrase. Returns NULL + * (with *length set to 0) if the value is NULL or invalid. + */ +static char* guac_ipmi_parse_k_g(guac_user* user, const char* raw, + int* length) { + + *length = 0; + + if (raw == NULL || *raw == '\0') + return NULL; + + /* Hexadecimal form: "0x..." decodes to arbitrary bytes */ + if (raw[0] == '0' && (raw[1] == 'x' || raw[1] == 'X')) { + + const char* hex = raw + 2; + size_t hex_len = strlen(hex); + + if (hex_len == 0 || (hex_len % 2) != 0) { + guac_user_log(user, GUAC_LOG_WARNING, "Hexadecimal K_g key must " + "have an even, non-zero number of digits; ignoring."); + return NULL; + } + + int bytes = (int) (hex_len / 2); + if (bytes > GUAC_IPMI_K_G_MAX_LENGTH) + guac_user_log(user, GUAC_LOG_WARNING, "K_g key exceeds the IPMI " + "maximum of %i bytes; the BMC may reject it.", + GUAC_IPMI_K_G_MAX_LENGTH); + + char* buffer = guac_mem_alloc(bytes); + for (int i = 0; i < bytes; i++) { + char nibbles[3] = { hex[i * 2], hex[i * 2 + 1], '\0' }; + if (!isxdigit((unsigned char) nibbles[0]) + || !isxdigit((unsigned char) nibbles[1])) { + guac_user_log(user, GUAC_LOG_WARNING, "K_g key contains an " + "invalid hexadecimal digit; ignoring."); + guac_mem_free(buffer); + return NULL; + } + buffer[i] = (char) strtol(nibbles, NULL, 16); + } + + *length = bytes; + return buffer; + + } + + /* Passphrase form: the raw bytes of the string */ + size_t raw_len = strlen(raw); + if (raw_len > GUAC_IPMI_K_G_MAX_LENGTH) + guac_user_log(user, GUAC_LOG_WARNING, "K_g key exceeds the IPMI " + "maximum of %i bytes; the BMC may reject it.", + GUAC_IPMI_K_G_MAX_LENGTH); + + char* buffer = guac_mem_alloc(raw_len); + memcpy(buffer, raw, raw_len); + *length = (int) raw_len; + return buffer; + +} + guac_ipmi_settings* guac_ipmi_parse_args(guac_user* user, int argc, const char** argv) { @@ -408,9 +681,13 @@ guac_ipmi_settings* guac_ipmi_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, IDX_PASSWORD, NULL); - settings->k_g = + /* Read the BMC key (K_g), decoding a "0x..." value as a raw byte string so + * that binary keys containing NUL bytes are preserved. */ + char* raw_k_g = guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, IDX_K_G, NULL); + settings->k_g = guac_ipmi_parse_k_g(user, raw_k_g, &settings->k_g_length); + guac_mem_free(raw_k_g); /* Read privilege level */ settings->privilege_level = guac_ipmi_parse_privilege_level(user, @@ -428,6 +705,21 @@ guac_ipmi_settings* guac_ipmi_parse_args(guac_user* user, "authentication, integrity, or confidentiality. Its use is " "strongly discouraged."); + /* Read encryption policy */ + settings->encryption_policy = guac_ipmi_parse_encryption_policy(user, + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_ENCRYPTION_POLICY, "required")); + + /* Read vendor workaround flags, resolving into separate SOL and chassis + * masks */ + char* workaround_value = + guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_WORKAROUND_FLAGS, NULL); + guac_ipmi_parse_workaround_flags(user, workaround_value, + &settings->sol_workaround_flags, + &settings->chassis_workaround_flags); + guac_mem_free(workaround_value); + /* Read SOL payload instance */ settings->sol_payload_instance = guac_user_parse_args_int(user, GUAC_IPMI_CLIENT_ARGS, argv, diff --git a/src/protocols/ipmi/settings.h b/src/protocols/ipmi/settings.h index 89a2ba13a5..151b0991f2 100644 --- a/src/protocols/ipmi/settings.h +++ b/src/protocols/ipmi/settings.h @@ -156,6 +156,33 @@ typedef enum guac_ipmi_boot_device { } guac_ipmi_boot_device; +/** + * A policy governing whether the negotiated RMCP+ cipher suite must provide + * confidentiality (payload encryption). The connection console and credentials + * are only protected on the wire when an encrypting cipher suite is used. + */ +typedef enum guac_ipmi_encryption_policy { + + /** + * Require an encrypting cipher suite. The connection is refused if the + * configured cipher suite does not provide confidentiality. This is the + * default. + */ + GUAC_IPMI_ENCRYPTION_REQUIRED = 0, + + /** + * Prefer an encrypting cipher suite, but allow a non-encrypting one with a + * logged warning. + */ + GUAC_IPMI_ENCRYPTION_PREFERRED, + + /** + * Allow any cipher suite, including those providing no confidentiality. + */ + GUAC_IPMI_ENCRYPTION_NONE + +} guac_ipmi_encryption_policy; + /** * Settings for the IPMI Serial-over-LAN connection. The values for this * structure are parsed from the arguments given during the Guacamole protocol @@ -192,10 +219,18 @@ typedef struct guac_ipmi_settings { /** * The BMC key (K_g) for two-key authentication, if any. NULL if - * unspecified, in which case the password is used as the BMC key. + * unspecified, in which case the password is used as the BMC key. This is + * a raw byte buffer that may contain NUL bytes; its length is given by + * k_g_length rather than being NUL-terminated for length purposes. */ char* k_g; + /** + * The length of the K_g key, in bytes. Zero if k_g is NULL. Stored + * separately because a binary K_g key may legitimately contain NUL bytes. + */ + int k_g_length; + /** * The privilege level to authenticate with. */ @@ -207,6 +242,25 @@ typedef struct guac_ipmi_settings { */ int cipher_suite; + /** + * The policy governing whether a non-confidential cipher suite is + * permitted. + */ + guac_ipmi_encryption_policy encryption_policy; + + /** + * Workaround flags applied to the libipmiconsole SOL session, as a bitwise + * OR of IPMICONSOLE_WORKAROUND_* values. Zero if no workarounds apply. + */ + unsigned int sol_workaround_flags; + + /** + * Workaround flags applied to the libfreeipmi chassis session, as a bitwise + * OR of IPMI_WORKAROUND_FLAGS_OUTOFBAND_2_0_* values. Zero if no workarounds + * apply. + */ + unsigned int chassis_workaround_flags; + /** * The SOL payload instance to use (1-15). */ @@ -388,6 +442,19 @@ guac_ipmi_settings* guac_ipmi_parse_args(guac_user* user, */ void guac_ipmi_settings_free(guac_ipmi_settings* settings); +/** + * Returns whether the given RMCP+ cipher suite ID provides confidentiality + * (payload encryption). Suites 0, 1, 2, 6, 7, 11, 15, and 16 (and any + * unrecognized suite) provide no confidentiality. + * + * @param cipher_suite + * The cipher suite ID to test. + * + * @return + * Non-zero if the cipher suite encrypts the payload, zero otherwise. + */ +int guac_ipmi_cipher_provides_confidentiality(int cipher_suite); + /** * NULL-terminated array of accepted client args. */ From c93354d326e1501955d0c934835767847788d330 Mon Sep 17 00:00:00 2001 From: Ciro Iriarte Date: Fri, 10 Jul 2026 17:21:46 -0300 Subject: [PATCH 04/14] GUACAMOLE-2300: IPMI: add boot-device-persistent and keepalive-interval parameters - boot-device-persistent: applies a boot device override persistently across all subsequent boots rather than only the next boot (the underlying chassis call already supported persistence; it was previously hardcoded to one-time). - keepalive-interval: sets the SOL keepalive interval (seconds), mapped to libipmiconsole's keepalive_timeout_len. Prevents BMCs that silently drop idle sessions (e.g. Supermicro after ~60s) from tearing down the console. --- src/protocols/ipmi/ipmi.c | 6 ++++-- src/protocols/ipmi/settings.c | 24 ++++++++++++++++++++++++ src/protocols/ipmi/settings.h | 13 +++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/protocols/ipmi/ipmi.c b/src/protocols/ipmi/ipmi.c index 80fb91f55f..fbea3e0639 100644 --- a/src/protocols/ipmi/ipmi.c +++ b/src/protocols/ipmi/ipmi.c @@ -225,7 +225,9 @@ static ipmiconsole_ctx_t __guac_ipmi_create_session(guac_client* client) { settings->timeout > 0 ? settings->timeout * 1000 : -1, .retransmission_timeout_len = -1, .retransmission_backoff_count = -1, - .keepalive_timeout_len = -1, + .keepalive_timeout_len = + settings->keepalive_interval > 0 + ? settings->keepalive_interval * 1000 : -1, .retransmission_keepalive_timeout_len = -1, .acceptable_packet_errors_count = -1, .maximum_retransmission_count = -1 @@ -286,7 +288,7 @@ static void __guac_ipmi_apply_connect_actions(guac_client* client) { /* Apply any boot device override prior to powering the system on */ if (settings->boot_device != GUAC_IPMI_BOOT_NONE) { if (guac_ipmi_chassis_set_boot_device(client, settings->boot_device, - false) == 0) + settings->boot_persistent) == 0) guac_client_log(client, GUAC_LOG_INFO, "Applied boot device override."); } diff --git a/src/protocols/ipmi/settings.c b/src/protocols/ipmi/settings.c index 8ae16d2303..5db44dd725 100644 --- a/src/protocols/ipmi/settings.c +++ b/src/protocols/ipmi/settings.c @@ -72,6 +72,8 @@ const char* GUAC_IPMI_CLIENT_ARGS[] = { "scrollback", "disable-copy", "disable-paste", + "boot-device-persistent", + "keepalive-interval", NULL }; @@ -271,6 +273,19 @@ enum IPMI_ARGS_IDX { */ IDX_DISABLE_PASTE, + /** + * Whether a configured boot device override should persist across + * subsequent boots rather than applying only to the next boot. Optional; + * defaults to false (next boot only). + */ + IDX_BOOT_DEVICE_PERSISTENT, + + /** + * The interval, in seconds, at which SOL keepalive packets are sent. Zero + * uses the libipmiconsole default. Optional. + */ + IDX_KEEPALIVE_INTERVAL, + IPMI_ARGS_COUNT }; @@ -672,6 +687,11 @@ guac_ipmi_settings* guac_ipmi_parse_args(guac_user* user, guac_user_parse_args_int(user, GUAC_IPMI_CLIENT_ARGS, argv, IDX_TIMEOUT, GUAC_IPMI_DEFAULT_TIMEOUT); + /* Read SOL keepalive interval (0 = libipmiconsole default) */ + settings->keepalive_interval = + guac_user_parse_args_int(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_KEEPALIVE_INTERVAL, 0); + /* Read credentials */ settings->username = guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, @@ -734,6 +754,10 @@ guac_ipmi_settings* guac_ipmi_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_IPMI_CLIENT_ARGS, argv, IDX_BOOT_DEVICE, "none")); + settings->boot_persistent = + guac_user_parse_args_boolean(user, GUAC_IPMI_CLIENT_ARGS, argv, + IDX_BOOT_DEVICE_PERSISTENT, false); + /* Read-only mode */ settings->read_only = guac_user_parse_args_boolean(user, GUAC_IPMI_CLIENT_ARGS, argv, diff --git a/src/protocols/ipmi/settings.h b/src/protocols/ipmi/settings.h index 151b0991f2..2af8a880dc 100644 --- a/src/protocols/ipmi/settings.h +++ b/src/protocols/ipmi/settings.h @@ -206,6 +206,13 @@ typedef struct guac_ipmi_settings { */ int timeout; + /** + * The interval, in seconds, at which SOL keepalive packets are sent to + * prevent idle BMCs from silently dropping the session. Zero requests the + * libipmiconsole default. + */ + int keepalive_interval; + /** * The username to authenticate with, if any. NULL if unspecified (the BMC * default / null username is used). @@ -280,6 +287,12 @@ typedef struct guac_ipmi_settings { */ guac_ipmi_boot_device boot_device; + /** + * Whether a configured boot device override should persist across all + * subsequent boots rather than applying only to the next boot. + */ + bool boot_persistent; + /** * Whether this connection is read-only, and user input should be dropped. */ From 3cf11d0b9448cd4c6cc703ef19da881db41349c8 Mon Sep 17 00:00:00 2001 From: Ciro Iriarte Date: Fri, 10 Jul 2026 17:22:02 -0300 Subject: [PATCH 05/14] GUACAMOLE-2300: IPMI: add System Event Log viewer and alternate-screen control menu - SEL viewer: guac_ipmi_chassis_sel() reads the most recent System Event Log entries via the libfreeipmi SEL parse API and formats them as text; exposed in the Ctrl+] control menu as [e] View Event Log. Invaluable for diagnosing crash/no-boot conditions while on the serial console. - Alternate screen: the control menu now renders on the terminal's alternate screen buffer (CSI ?1049h/l), so it no longer pollutes the serial console's scrollback and session recording, nor corrupts a full-screen application drawn on the primary screen. Command output is held on the alternate screen until dismissed with a keypress, then the console is restored intact. --- src/protocols/ipmi/chassis.c | 123 +++++++++++++++++++++++++++++++++++ src/protocols/ipmi/chassis.h | 18 +++++ src/protocols/ipmi/ipmi.h | 7 ++ src/protocols/ipmi/menu.c | 81 ++++++++++++++++++++--- 4 files changed, 221 insertions(+), 8 deletions(-) diff --git a/src/protocols/ipmi/chassis.c b/src/protocols/ipmi/chassis.c index b671b3cf17..c01be84f70 100644 --- a/src/protocols/ipmi/chassis.c +++ b/src/protocols/ipmi/chassis.c @@ -368,3 +368,126 @@ int guac_ipmi_chassis_identify(guac_client* client, int interval, bool force) { return result; } + +/** + * The maximum number of System Event Log entries retained by the viewer (the + * most recent entries). + */ +#define GUAC_IPMI_SEL_MAX_ENTRIES 20 + +/** + * The maximum length, in bytes, of a single formatted SEL entry line. + */ +#define GUAC_IPMI_SEL_LINE_LENGTH 160 + +/** + * State accumulated while parsing the System Event Log. Formatted entries are + * stored in a ring buffer such that, after parsing, the buffer holds the most + * recent GUAC_IPMI_SEL_MAX_ENTRIES entries. + */ +typedef struct guac_ipmi_sel_state { + + /** + * Ring buffer of formatted SEL entry lines. + */ + char lines[GUAC_IPMI_SEL_MAX_ENTRIES][GUAC_IPMI_SEL_LINE_LENGTH]; + + /** + * The total number of SEL records parsed so far. + */ + int total; + +} guac_ipmi_sel_state; + +/** + * libfreeipmi SEL parse callback. Formats the current SEL record into the ring + * buffer maintained by the given guac_ipmi_sel_state. + * + * @param sel_ctx + * The SEL parse context, positioned at the record to format. + * + * @param data + * A pointer to the guac_ipmi_sel_state accumulating formatted entries. + * + * @return + * Zero to continue parsing. + */ +static int guac_ipmi_sel_callback(ipmi_sel_ctx_t sel_ctx, void* data) { + + guac_ipmi_sel_state* state = (guac_ipmi_sel_state*) data; + char* slot = state->lines[state->total % GUAC_IPMI_SEL_MAX_ENTRIES]; + + /* Format "