GUACAMOLE-2300: Add IPMI Serial-over-LAN protocol support#692
GUACAMOLE-2300: Add IPMI Serial-over-LAN protocol support#692ciroiriarte wants to merge 14 commits into
Conversation
…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 #1.
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.
…ine 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().
…al 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.
…n 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.
Chassis operations (power, status, identify, SEL) opened a fresh IPMI session synchronously from the user input thread, freezing that user's input for the 3-5s RMCP+ handshake. They now run on a background worker thread: the menu prints a 'working...' message immediately and renders the result when the worker completes, keeping the console responsive. Only one operation runs at a time (guarded by chassis_busy under menu_lock); control-menu keystrokes are ignored while an operation is in flight. The worker thread is joined in the free handler before the terminal is destroyed, so an abrupt disconnect mid-operation cannot cause a use-after-free (verified by a disconnect-during-op stress test). Serial break remains synchronous as it is fast and operates on the existing SOL context.
…ic control/status Implements the out-of-band control/status channel (#28) that keeps chassis control and telemetry off the SOL console stream, so any client (browser panel, native TUI) can render it natively. The in-terminal Ctrl+] menu remains a portable fallback. A client opens an inbound pipe named 'ipmi-control' and sends newline-delimited JSON commands; the server pushes JSON state/result/SEL messages back on outbound 'ipmi-control' pipes: {"id":"..","type":"command","command":"power-cycle"} {"type":"state","power":"on","health":"sol-connected"} {"id":"..","type":"result","ok":true,"message":".."} {"type":"sel","text":".."} Commands: power on/off/cycle/hard-reset/soft-shutdown/diagnostic-interrupt, identify, send-break, refresh-status, read-sel. Chassis operations serialize with the Ctrl+] menu via the shared chassis lock, and the channel is only available to non-read-only users (the pipe handler is not registered for read-only). Validated end-to-end against a Supermicro BMC (state query, identify, SEL read, error handling). Follow-ups: structured per-entry SEL, async command dispatch, periodic state polling / push-on-change, passive state broadcast to read-only viewers.
The out-of-band control channel reported SOL health as a one-shot snapshot taken when the panel first opened the ipmi-control pipe. Since the browser connects and opens that pipe while the SOL session is still being established (the blocking ipmiconsole_engine_submit_block on the client thread), the snapshot captured console_ctx == NULL and the panel showed "SOL DISCONNECTED" even while the console streamed live output — correcting only after a manual Refresh. Track SOL liveness in a dedicated, mutex-guarded sol_connected flag (separate from console_ctx, which stays non-NULL until teardown) and broadcast the state to all connected users via guac_client_foreach_user() immediately after the session establishes and again when the read loop exits. The push is health-only (omits power) so it refreshes the connection badge without regressing the last-known power state to "unknown"; an actively-queried power value is still sent for refresh-status. Server-only change; the existing client handler already ignores an absent power field. Verified against a Supermicro BMC: the badge reaches SOL CONNECTED on its own with power intact, no manual refresh.
…nd EAGAIN __guac_ipmi_write_all() treated any write() result <= 0 as fatal and broke the input-forwarding loop, which would silently stop forwarding terminal keystrokes to the SOL session on a transient condition. Handle partial writes by continuing with the remainder, and, should the libipmiconsole descriptor ever be non-blocking, wait for POLLOUT and retry on EAGAIN/EWOULDBLOCK rather than dropping input. Any real error or EOF remains fatal. No behavioural change for the current blocking descriptor.
Add CUnit tests (wired into 'make check' like the Kubernetes/RDP module tests) for the two pure helpers that parse untrusted, browser-supplied control-channel messages: guac_ipmi_control_json_get() and guac_ipmi_control_json_escape(), which are now exposed via control.h for testing. Coverage includes key-vs-value matching (a token appearing only as a value must not be read as a key), whitespace tolerance, escape handling/unescaping, dropping of non-printable control characters, and output-buffer bounds. 12 cases, all passing.
…terminal guac_ipmi_client_free_handler() unconditionally called guac_terminal_free() on ipmi_client->term, but the terminal is created by the client thread; if the connection is torn down before that thread reaches terminal creation, term is still NULL and guac_terminal_free() -> guac_terminal_stop() dereferences it, crashing the connection process. Guard the free with a NULL check. Found via an AddressSanitizer run over a rapid connect/teardown of the ipmi-control channel; fix confirmed clean under ASAN.
…fore freeing the terminal The connection free handler freed the terminal before joining the SOL worker thread, and the join was gated on console_ctx being non-NULL. The worker's read loop writes SOL output to the terminal (holding term->lock), so freeing the terminal first could destroy the lock/buffers while the worker was mid-write -> use-after-free. The conditional join also leaked the joinable worker thread on every session-setup failure path (terminal init, encryption-required abort, engine-ref failure, SOL failure), which all leave console_ctx NULL. Mirror the SSH module's ordering: close the SOL fd and guac_terminal_stop() to unblock both threads without freeing the terminal, join the worker unconditionally (tracked by a new client_thread_valid flag, like chassis_thread_valid), then destroy the SOL context and finally free the terminal. Found by independent code + security review; verified clean under AddressSanitizer.
…ak, bounded printf Address remaining code/security review findings: - guac_ipmi_settings_free() now explicit_bzero()s the password and the decoded K_g BMC key before freeing, so these secrets do not linger in freed heap. - send-break (control channel and Ctrl+] menu) now tests sol_connected under state_lock before dereferencing console_ctx, closing a narrow race with SOL teardown. - argv.c uses snprintf() instead of sprintf() into the fixed font-size buffer. Menu-state cross-user locking, alt-screen-vs-live-SOL suppression, and the dead identify 'force' parameter are noted as follow-ups.
Thread-safety and cleanup items surfaced by the code/security review: - Serialize the shared control-menu state (menu_open, menu_pending_action, menu_awaiting_dismiss) under menu_lock: the key handler now snapshots it in one short critical section and the menu open/close/confirm paths mutate it under the lock, eliminating the data race across concurrent user key threads and the chassis worker. Critical sections are kept short (no lock held across thread creation or terminal writes) to avoid deadlock. - Suppress SOL console output while the control menu owns the alternate screen, so live serial data no longer scrambles the rendered menu. - Serialize status/SEL reads with the other chassis operations (they now take the chassis lock too) so a single connection cannot open many concurrent RMCP+ sessions and exhaust the BMC's session slots. - guac_ipmi_chassis_status() now outputs a decoded guac_ipmi_power_state enum; the control channel uses it instead of substring-matching the human-readable status string. - Drop the always-false 'force' parameter from guac_ipmi_chassis_identify(). Built, unit-tested (12/12), functionally verified (status/identify/SEL), and re-checked clean under AddressSanitizer (no UAF/leak/deadlock).
necouchman
left a comment
There was a problem hiding this comment.
@ciroiriarte This looks really great, I'm excited to have this in the Guacamole code! I do have some initial comments - don't despair, most of these are style and documentation clean-up, although some deal with implementation details. Generally:
- In several places you've defined Guacamole-specific data structures or enums that mirror ones provided by the IPMI libraries. Is there any reason to do this? It seems like it would simplify things and cut down the amount of code to just reuse what is already there, unless there's a technical or implementation-specific reason why not?
- Make sure you've fully-documented all of the functions and/or function prototypes. There are several places where thinks like
@paramand@returntags are missing, and a handful where there was no documentation. - There are a few places where I think some more verbose comments within the code would help people more easily understand what's being done and why.
- I see what you're doing with the IPMI menu, but I wonder if there's a way to use Guacamole's built-in menu to handle these functions rather than implementing the text-based menu you have and then having to implement the JSON-related code around it? I haven't looked at this in any detail, it just seems to me that that's what our existing hidden Guacamole menu is meant for, and there ought to be a way to handle things like chassis commands and the like similar to how we handle file transfers, terminal parameters, etc.
| /** | ||
| * Verifies that a non-string value (e.g. a number) is not matched, as only | ||
| * quoted string values are extracted. | ||
| */ | ||
| void test_json_get__non_string_value(void) { | ||
| char out[64]; | ||
| CU_ASSERT_FALSE(guac_ipmi_control_json_get( | ||
| "{\"n\":123}", "n", out, sizeof(out))); | ||
| } |
There was a problem hiding this comment.
Why are only string values allowed? My experience in general with JSON is that string values are expected for strings, but other values - boolean, numeric, etc. - are perfectly valid JSON?
| * under the License. | ||
| */ | ||
|
|
||
| #include "config.h" |
There was a problem hiding this comment.
Mike recently submitted a PR that makes including config.h unnecessary - see #691.
| /** | ||
| * 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; | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
Is there a particular reason we need to define new values for these privilege levels as GUAC_ constants and shouldn't just re-use the IPMI_PRIVILEGE_LEVEL_ values throughout the code?
| /** | ||
| * 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; | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
Similar to the privilege levels - is there a particular reason that we need to create and map them rather than just using the ones provided by FreeIPMI?
| /** | ||
| * 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; | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
And, similar to above two - why do we need to redefine these?
| /** | ||
| * 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) { |
There was a problem hiding this comment.
Please document parameters and return value.
| if (raw == NULL || *raw == '\0') | ||
| return NULL; |
There was a problem hiding this comment.
Might be good to log this at WARN or DEBUG level?
| 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; |
There was a problem hiding this comment.
Please document each of the values. Also, as mentioned in one of the other files, is there any reason not to just use the IPMICONSOLE_PRIVILEGE_* values?? Why redefine them?
| /** | ||
| * 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; |
There was a problem hiding this comment.
When you say "any other IPMI connections active within the same guacd process" - does this mean:
- Within the same guacd parent process?
- Within the same guacd child/connection process?
guacd forks a new process for each connection, and, since this particular lock would only be initialized in the context of a single connection, and that would be after the child connection has been forked by guacd, there actually should not be any issue with initializing it or tearing it down...
...UNLESS, you also mean that the IPMI chassis connections would interfere with the SOL connection, in which case, this would still make sense.
| /** | ||
| * Displays a "press any key to return" prompt and marks the current control | ||
| * menu operation complete. Forward-declared for use by the chassis worker. | ||
| * | ||
| * @param client | ||
| * The guac_client associated with the IPMI connection. | ||
| */ | ||
| static void guac_ipmi_menu_await_dismiss(guac_client* client); |
There was a problem hiding this comment.
Our practice throughout the code for static functions is generally just to implement them where they need to be in the code and not prototype them.
This adds a new
ipmiprotocol module to guacamole-server providing Serial-over-LAN (SOL) console access and chassis power management for BMCs (baseboard management controllers), built on FreeIPMI (libipmiconsolefor SOL,libfreeipmifor chassis control).JIRA: https://issues.apache.org/jira/browse/GUACAMOLE-2300
What's included
libguac-client-ipmiprotocol module — SOL console rendered through the sharedguac_terminal.ipmi-controlpipe channel (newline-delimited JSON): power on/off/cycle/reset, soft shutdown, diagnostic interrupt (NMI), chassis identify, System Event Log (SEL) viewer, and live power/SOL status.Ctrl+]) with an alternate-screen UI, run asynchronously so BMC round-trips never block the console.K_g, encryption policy. Verified end-to-end against Supermicro (ATEN), Lenovo XCC, and Dell iDRAC 9.make check.Testing
Related PRs