Skip to content

GUACAMOLE-2300: Add IPMI Serial-over-LAN protocol support - #692

Open
ciroiriarte wants to merge 16 commits into
apache:mainfrom
ciroiriarte:feature/ipmi-protocol
Open

GUACAMOLE-2300: Add IPMI Serial-over-LAN protocol support#692
ciroiriarte wants to merge 16 commits into
apache:mainfrom
ciroiriarte:feature/ipmi-protocol

Conversation

@ciroiriarte

@ciroiriarte ciroiriarte commented Jul 13, 2026

Copy link
Copy Markdown

This adds a new ipmi protocol module to guacamole-server providing Serial-over-LAN (SOL) console access and chassis power management for BMCs (baseboard management controllers), built on FreeIPMI (libipmiconsole for SOL, libfreeipmi for chassis control).

JIRA: https://issues.apache.org/jira/browse/GUACAMOLE-2300

What's included

  • New libguac-client-ipmi protocol module — SOL console rendered through the shared guac_terminal.
  • Chassis control over a client-agnostic ipmi-control pipe 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.
  • In-terminal control menu (Ctrl+]) with an alternate-screen UI, run asynchronously so BMC round-trips never block the console.
  • Multivendor support: configurable cipher suite / privilege level / workaround flags, hex K_g, encryption policy. Verified end-to-end against Supermicro (ATEN), Lenovo XCC, and Dell iDRAC 9.
  • Connection parameters including persistent boot-device selection and keepalive interval.
  • CUnit tests for the control-channel JSON parser, wired into make check.

Testing

  • 12/12 unit tests pass.
  • AddressSanitizer clean on live SOL sessions. Two memory bugs were found and fixed during review: a NULL-deref on teardown, and a use-after-free that freed the terminal before joining the SOL worker.
  • Full-stack live regression (browser → guacamole-client → guacd → BMC): SOL console I/O, live status push, chassis identify, SEL read, and Send Break all verified non-destructively against a Supermicro BMC.

Related PRs

…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 necouchman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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 @param and @return tags 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.

Comment on lines +76 to +84
/**
* 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)));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@ciroiriarte ciroiriarte Jul 17, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guac_ipmi_control_json_get() bails if the value isn't quoted, so {"id":123} from a third-party client parses as absent. Given your point about independent client implementations, I'll extend it to accept numeric and boolean scalars.

Comment thread src/protocols/ipmi/argv.c Outdated
* under the License.
*/

#include "config.h"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mike recently submitted a PR that makes including config.h unnecessary - see #691.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 86f6677 — config.h is auto-included in every .c file since GUACAMOLE-2182, so the explicit include is redundant.

Comment on lines +51 to +74
/**
* 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;
}

}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same privilege question — answered in full under the enum definition in settings.h (#692 (comment)). Short version: libipmiconsole and libfreeipmi disagree on the privilege values (0/1/2 vs 0x02/0x03/0x04), so reusing either is silently wrong on the other path. I'll keep an internal enum but give it arbitrary values and map explicitly on both paths.

Comment on lines +76 to +105
/**
* 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;
}

}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Different rationale from the privilege levels here, and a weaker one — I'd rather be upfront about that than stretch the same argument to cover it. There's no conflicting-constants problem for power actions or boot devices; these enums are less a wrapper around the FreeIPMI constants than internal command state. Each carries a NONE member that has no IPMI equivalent (IPMI_CHASSIS_CONTROL_* can't express "no action requested"), and they're used well away from this mapping — parsing the client's command strings, the menu's display labels, and the pending-confirmation state in guac_ipmi_client. The translation to the FreeIPMI constant happens only here, at the boundary.

NONE could be modelled as optional/nullable state instead, which would let these collapse into the raw constants. I lean toward keeping them as internal state enums for the parsing and menu use, but if you'd prefer the nullable approach I'll switch.

Comment on lines +107 to +132
/**
* 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;
}

}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And, similar to above two - why do we need to redefine these?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same rationale as the power-action enum just above (#692 (comment)): these are internal command state with a NONE sentinel that has no IPMI_CHASSIS_CONTROL_* / boot-device equivalent, mapped to the FreeIPMI constant only here at the boundary.

Comment on lines +591 to +599
/**
* 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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document parameters and return value.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented in 86f6677.

Comment thread src/protocols/ipmi/settings.c Outdated
Comment on lines +603 to +604
if (raw == NULL || *raw == '\0')
return NULL;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to log this at WARN or DEBUG level?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a DEBUG log for this case in 86f6677.

Comment on lines +75 to +79
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things here — the "why redefine" question and the request to document the values.

On reuse: this is the one enum I'd argue for keeping, because the module talks to two libraries whose privilege constants don't agree. SOL goes through libipmiconsole (IPMICONSOLE_PRIVILEGE_USER/OPERATOR/ADMIN = 0/1/2); chassis goes through libfreeipmi (IPMI_PRIVILEGE_LEVEL_USER/OPERATOR/ADMIN = 0x02/0x03/0x04). So "just reuse the library's values" is ambiguous, and a value picked for one library is silently wrong on the other path — an IPMICONSOLE_PRIVILEGE_ADMIN (2) reaching the chassis code reads as IPMI_PRIVILEGE_LEVEL_USER (0x02), a privilege downgrade with nothing to flag it.

The current code does invite the question fairly, though: the enum is defined as USER=0, OPERATOR=1, ADMIN=2 — hard-coded to the libipmiconsole numbering — and ipmi.c assigns it straight into ipmiconsole_ipmi_config.privilege_level with no mapping, while only the chassis side maps (via guac_ipmi_map_privilege_level()). So on the SOL path the enum really is just a copy of libipmiconsole's constants, which is why it reads as redundant. I'll give the values arbitrary numbering (not 0/1/2) and add a companion guac_ipmi_map_privilege_level_ipmiconsole() mirroring the chassis switch, so neither back-end ever receives the raw enum value.

To be straight about it: two conflicting constant sets don't prove a third enum is the only option — parsing directly to each library's constant at its boundary would work too. The internal enum just keeps the parsed setting semantic between the settings layer and the two back-ends instead of committing to one library's numbering at parse time. If you'd rather drop it and map at the boundary, I'll do that instead.

On the documentation: I'll add a doc comment to each value as part of that change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up from above: this is now done in c29f725. The enum values are library-independent (INVALID = 0, then USER/OPERATOR/ADMIN), and the SOL path maps through a new guac_ipmi_map_privilege_level_ipmiconsole() mirroring the chassis-side guac_ipmi_map_privilege_level() — so neither back-end receives the raw enum value anymore.

Comment thread src/protocols/ipmi/ipmi.c Outdated
Comment on lines +45 to +51
/**
* 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is misleading, not just unclear. This lock has nothing to do with chassis-vs-SOL interference; that's handled separately by menu_lock/chassis_busy in control.c. All this guards is the refcount around ipmiconsole_engine_init()/ipmiconsole_engine_teardown(), and your reading of the fork model is the right one: guacd forks per connection, the refcount lives in that child, so it can only ever go 0→1 and the lock never contends. As written it's scaffolding for a sharing scenario that can't occur.

Let me simplify it rather than justify it. If it's worth keeping any of it, the argument would be that libipmiconsole's engine is process-global and the refcount is cheap insurance if guacd ever ran connections in threads rather than forks — but that's speculative, and I'd rather not carry speculative concurrency machinery. I'll cut it back to a plain init/teardown and fix the comment.

Comment thread src/protocols/ipmi/menu.c Outdated
Comment on lines +62 to +69
/**
* 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 86f6677 — dropped the prototype and moved the definition ahead of its first use.

Addresses the review on apache#692:

Behavior:
* Accept non-string JSON scalars (numbers, true/false/null) in the
  control-channel parser, so a valid message from a third-party client
  is no longer misread as a missing key.
* Return guac_ipmi_power_state directly from guac_ipmi_chassis_status()
  instead of a result code plus an out-parameter.
* Reject unknown control commands explicitly via a final else branch
  rather than falling through to the power-action path.
* Drop the vestigial libipmiconsole engine reference count and its
  mutex; guacd forks per connection, so the count could only ever be
  0->1. Init/teardown the engine directly.

Settings parsers:
* Make the privilege-level, power-action, boot-device, and
  encryption-policy parsers pure string-to-enum converters: no logging
  and no default policy. Each returns a new *_INVALID sentinel for
  NULL/empty/unrecognized input, and the default plus warning is applied
  by the caller. This also fixes a small leak where the parsed argument
  strings were never freed.
* Collapse the redundant "administrator" privilege alias to "admin".
* Remove the unused RMCP "port" setting entirely; both libipmiconsole
  and libfreeipmi hardwire 623/UDP, so it was read but never honored.

Documentation and style:
* Complete @param/@return documentation across control.c and settings.c,
  document the workaround struct members and each enum value, and drop
  the doc blocks duplicated from control.h.
* Remove the now-redundant "config.h" includes (auto-included since
  GUACAMOLE-2182).
* One case label per line, move file-level defines/typedefs to the top
  of chassis.c, and inline the menu.c static function rather than
  forward-declaring it.
* Add DEBUG log lines when no workaround flags or K_g key are given.
@ciroiriarte

Copy link
Copy Markdown
Author

Thanks again for the thorough review, @necouchman — I've pushed 86f6677 addressing the feedback, and replied inline on each thread. A summary of what changed:

Documentation

  • Completed @param/@return across control.c and settings.c, documented the workaround struct members and each enum value, and removed the doc blocks that duplicated control.h.

Style

  • Dropped the now-redundant config.h includes (auto-included since GUACAMOLE-2182), one case label per line, moved file-level #define/typedef declarations to the top of chassis.c, and inlined the menu.c static function instead of forward-declaring it.

Settings parsers

  • The privilege / power-action / boot-device / encryption-policy parsers are now pure string-to-enum converters — no logging, no defaults. Each returns a new *_INVALID sentinel and the default plus warning is applied by the caller (this also fixed a small leak where the parsed argument strings were never freed). Collapsed the administrator alias to admin.
  • Removed the unused port setting entirely — both libipmiconsole and libfreeipmi hardwire 623/UDP, so it was read but never honored.

Behavior (discussed inline)

  • Accept non-string JSON scalars in the control parser, return the power state directly from guac_ipmi_chassis_status(), reject unknown control commands explicitly, and drop the vestigial engine reference count/lock.

Still to come, from the enum discussion above: giving the privilege enum library-independent values and adding the symmetric guac_ipmi_map_privilege_level_ipmiconsole() mapper so neither back-end receives the raw enum value. I'll follow up with that shortly. Happy to iterate on anything here.

guac_ipmi_privilege_level previously took the same numeric values as
libipmiconsole's IPMICONSOLE_PRIVILEGE_* constants, and the SOL path
passed the enum straight into the libipmiconsole config struct. That was
correct only by coincidence: libfreeipmi (used by the chassis path)
numbers its privilege constants differently, and any future reordering
of the enum or renumbering by the library would have silently sent the
wrong privilege level.

Give the enum library-independent values (INVALID = 0, so a
zero-initialized field is safe) and add guac_ipmi_map_privilege_level_
ipmiconsole(), mirroring the existing chassis-side mapper, so each
back-end is handed its own constant via an explicit, compiler-checked
switch rather than the raw enum value.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants