Skip to content

GUACAMOLE-2281: Add generic req/done/cancel instructions to libguac.#675

Open
aleitner wants to merge 1 commit into
apache:mainfrom
aleitner:GUACAMOLE-2281-webauthn-passthrough
Open

GUACAMOLE-2281: Add generic req/done/cancel instructions to libguac.#675
aleitner wants to merge 1 commit into
apache:mainfrom
aleitner:GUACAMOLE-2281-webauthn-passthrough

Conversation

@aleitner

@aleitner aleitner commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Adds a generic request/response RPC primitive to the Guacamole protocol in the form of three instructions: "req", "done", and "cancel".

A "req" instruction initiates a typed request with three arguments: a correlation id, a method name (by convention namespaced like "."), and an opaque payload. The receiving side is expected to settle the request with a "done" instruction carrying the same id, a status (typically "ok", "error", or "canceled"), and a response payload. Either side may issue a "cancel" with the matching id to abort an in-flight request before its "done" arrives.

The instructions themselves carry no feature-specific semantics; the method name argument identifies what kind of request is being made, and the payload is opaque to libguac. This leaves room for any RPC-style feature to share the same plumbing without needing its own dedicated opcodes.

Adds the libguac surface for the new primitive:

  • guac_protocol_send_req, guac_protocol_send_done, and guac_protocol_send_cancel in protocol.h / protocol.c.
  • guac_user_req_handler, guac_user_done_handler, and guac_user_cancel_handler typedefs in user-fntypes.h.
  • req_handler, done_handler, and cancel_handler callback fields on guac_user in user.h.
  • __guac_handle_req, __guac_handle_done, and __guac_handle_cancel dispatchers in user-handlers.c, registered in the instruction handler map.

The first intended consumer is WebAuthn passthrough, which will use this primitive to relay ceremonies between protocol plugins and the user's local authenticator with method names "webauthn.create" and "webauthn.get". The primitive is deliberately generic so other RPC-style features can adopt it without adding more opcodes to the protocol vocabulary.

Guacamole client PR

Comment thread src/libguac/protocol.c Outdated
@aleitner aleitner force-pushed the GUACAMOLE-2281-webauthn-passthrough branch 3 times, most recently from e5c7912 to 4eca0a8 Compare June 4, 2026 08:25
@bbennett-ks

Copy link
Copy Markdown
Contributor

Maybe add a UT for the new framework? Similar to ./src/libguac/tests/socket/fd_send_instruction.c? May be worthwhile, since new framework code paths aren't exercise by the the Guacamole product.

@bbennett-ks

Copy link
Copy Markdown
Contributor

Thinking through shared sessions: since send_req() broadcasts to all connected users, we would receive multiple "done" responses for a single request. Could we follow the existing guac_client_owner_send_required() pattern and target only the session owner instead of broadcasting?

This follows the existing RDP & VNC pattern of prompting only the session owner for authentication-related input.

Comment thread src/libguac/guacamole/protocol.h Outdated
Comment thread src/libguac/guacamole/protocol.h
Comment thread src/libguac/user-handlers.c Outdated
Comment thread src/libguac/user-handlers.c Outdated
Comment thread src/libguac/user-handlers.c Outdated
Comment thread src/libguac/user-handlers.c Outdated
Comment thread src/libguac/protocol.c Outdated
Comment thread src/libguac/user-handlers.c Outdated
Comment thread src/libguac/user-handlers.c Outdated
Comment thread src/libguac/user-handlers.c Outdated
Comment thread src/libguac/user-handlers.c Outdated
@bbennett-ks

Copy link
Copy Markdown
Contributor

I think this (and it's companion: apache/guacamole-client#1218) are actually drafts & not intended to be merged at this point. Maybe we should change the PR status?

@necouchman necouchman marked this pull request as draft June 5, 2026 01:13
@necouchman

Copy link
Copy Markdown
Contributor

I think this (and it's companion: apache/guacamole-client#1218) are actually drafts & not intended to be merged at this point. Maybe we should change the PR status?

Okay, I've marked them as Draft for now.

@aleitner aleitner force-pushed the GUACAMOLE-2281-webauthn-passthrough branch 4 times, most recently from 8258dcc to 1688641 Compare June 8, 2026 06:07
@aleitner

aleitner commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Added unit tests in the latest push

@aleitner aleitner marked this pull request as ready for review June 8, 2026 06:10

@bbennett-ks bbennett-ks 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.

I looked over the existing primitives (msg, pipe, argv) to understand the need for a new RPC set, and this is a good extension: none of them offer a correlated request/response, whereas this provides:

  • Bidirectional request/response channel (either side can initiate).
  • Client-generated UUID correlation that stays unique across the multiple processes in the round-trip workflow (where a per-connection stream/object index couldn't).
  • Payload breakup & reassembly: the body streams over a paired pipe as blobs and is reassembled by libguac, allowing payloads larger than a single instruction can carry.
  • Cancellation support.

@aleitner aleitner force-pushed the GUACAMOLE-2281-webauthn-passthrough branch 2 times, most recently from 5e30d6f to 6923c20 Compare June 9, 2026 03:07

@mike-jumper mike-jumper 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.

I'm mainly not sure about the set of req/done/cancel instructions and use of pipe. The structure resembles the established "object" pattern that we already use for filesystem, so in this case I'd lean more toward an object-style service that can be given a name.

For example:

  1. service declares a service object named webauthn.
  2. Client retrieves the object's JSON index of available streams, which here would be create and get.
  3. When the client wishes to issue a request, it creates a put stream to the relevant named stream of the object, including an opaque request ID in the path (ie: create/SOME_ID).
  4. When the server needs to send the response for a request, it issues a body for the same path.

Would that align with what you're seeing with the structure of WebAuthN (and possibly plans for USB)?

@aleitner

Copy link
Copy Markdown
Contributor Author

I'm mainly not sure about the set of req/done/cancel instructions and use of pipe. The structure resembles the established "object" pattern that we already use for filesystem, so in this case I'd lean more toward an object-style service that can be given a name.

For example:

1. `service` declares a service object named `webauthn`.

2. Client retrieves the object's JSON index of available streams, which here would be `create` and `get`.

3. When the client wishes to issue a request, it creates a `put` stream to the relevant named stream of the object, including an opaque request ID in the path (ie: `create/SOME_ID`).

4. When the server needs to send the response for a request, it issues a `body` for the same path.

Would that align with what you're seeing with the structure of WebAuthN (and possibly plans for USB)?

A couple clarifications before reshaping the PR.

WebAuthn passthrough is server initiated. The server forwards a challenge from upstream and the browser is the responder. The existing object pattern has get and put flowing client to server with body as the response back, and I couldn't find an example of a server-pushed body without a preceding client get. Would the shape you have in mind involve a server-issued put toward the client carrying the request, or is there an existing primitive for server-initiated push I'm missing

@mike-jumper

mike-jumper commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

A challenge/response flow could just be:

  1. An auth-challenge stream, with the challenge having an arbitrary challenge ID distinct from the stream ID. The fact that the challenge is for consumption via WebAuthN could be embedded into the mimetype.
  2. An auth-response stream, with the response pointing at the challenge ID.

I add the auth- prefixes here just because having a naked response opcode feels like we're commandeering a broad term for too specific a purpose.

@aleitner aleitner force-pushed the GUACAMOLE-2281-webauthn-passthrough branch from 6923c20 to 9be1af5 Compare June 11, 2026 05:41
@aleitner

Copy link
Copy Markdown
Contributor Author

A challenge/response flow could just be:

1. An `auth-challenge` stream, with the challenge having an arbitrary challenge ID distinct from the stream ID. The fact that the challenge is for consumption via WebAuthN could be embedded into the mimetype.

2. An `auth-response` stream, with the response pointing at the challenge ID.

I add the auth- prefixes here just because having a naked response opcode feels like we're commandeering a broad term for too specific a purpose.

Okay so I am working on a redesign. Two new stream-opening instructions, auth-challenge and auth-response, each announcing a stream the body rides on directly. This way we have no paired pipe, reserved mimetype, or single-slot per-user state.

auth-challenge,<stream_idx>,<mimetype>,<challenge_id>
auth-response,<stream_idx>,<mimetype>,<challenge_id>

The mimetype identifies the ceremony kind. I went with application/x-webauthn-create+json and application/x-webauthn-get+json

Comment thread src/libguac/guacamole/stream.h Outdated
* }
* @endcode
*/
void (*destructor)(void* data);

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.

The established pattern here is a free_handler, which would need its own stream-specific typedef (just like the other structs).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed to free_handler and added a guac_stream_free_handler typedef in user-fntypes.h alongside the other stream handler typedefs

Comment thread src/libguac/user.c Outdated
Comment on lines +148 to +153
/* Run the registered destructor for the stream's data, if any */
if (stream->destructor != NULL) {
stream->destructor(stream->data);
stream->destructor = NULL;
stream->data = 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.

Good idea to NULL out stream->data here, but that should probably be done more broadly and regardless of whether a destructor is provided. For example:

if (stream->destructor != NULL)
    stream->destructor(stream->data);

/* Clean up any remaining dangling pointers before permitting reuse of the
 * stream */
stream->data = NULL;
stream->destructor = NULL;
stream->another_handler = NULL;
stream->yet_another_handler = NULL;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Moved the cleanup out of the if-block. data, ack_handler, blob_handler, end_handler, and free_handler are now all NULL'd unconditionally before the slot is released. Same fix in guac_client_free_stream

Comment on lines +35 to +40
typedef struct auth_capture {
int call_count;
int stream_index;
char mimetype[64];
char challenge_id[64];
} auth_capture;

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added docs to each field

Comment on lines +42 to +49
/**
* Handler that copies its arguments into the user's data pointer
* (assumed to be an auth_capture) so the test can assert them. Used for
* both the auth-challenge and auth-response paths since they share the
* same handler signature.
*/
static int capture_auth(guac_user* user, guac_stream* stream,
char* mimetype, char* challenge_id) {

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.

We need to always document all parameters and return values.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Documented the params and return


}

static void teardown_auth_user(guac_user* user) {

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 this function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Documented setup_auth_user and teardown_auth_user

Comment thread src/libguac/tests/protocol/auth_send.c Outdated
* Sender for the auth-response test: emits send_auth_response referencing
* an earlier challenge id.
*/
static void write_auth_response(int fd) {

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.

Same here - we need to document all parameters.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed along with send_auth_response, nothing emits auth-response from the server. See the map entry for the removed auth-challenge

Comment thread src/libguac/user-handlers.c Outdated
{"usbconnect", __guac_handle_usbconnect},
{"usbdata", __guac_handle_usbdata},
{"usbdisconnect", __guac_handle_usbdisconnect},
{"auth-challenge", __guac_handle_auth_challenge},

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.

What are the semantics of receiving an auth challenge from the user?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed auth-challenge since nothing registers a handler. Also dropped send_auth_response, the typedef, and the field since the server only emits auth-challenge and only receives auth-response. Same on the JS side, dropped the auth-response handler, onauthresponse, and sendAuthChallenge. Can add the reverse direction back if anything else needs it

Comment on lines +61 to +62
static void run_sender_check(void (*sender)(int), int write_fd, int read_fd,
const char* expected, int expected_len) {

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.

Here, too, please - all parameters need to be documented.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Documented all params

* heap-allocated buffer, returning the buffer and writing the length to
* *out_len. Caller takes ownership of the buffer. Closes the file descriptor.
*/
static char* read_all(int fd, int* out_len) {

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 here - documentation needed for parameters and return values.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Documented the params and return

@aleitner aleitner force-pushed the GUACAMOLE-2281-webauthn-passthrough branch from 9be1af5 to 3a1ec95 Compare June 17, 2026 03:59
…libguac.

Adds two new protocol instructions for authentication exchanges:

 - auth-challenge: opens a stream carrying the body of a challenge for
   a pending authentication exchange. The wire form is
   "auth-challenge,<stream_idx>,<mimetype>,<challenge_id>". The mimetype
   identifies the auth flavor; the challenge_id is an opaque identifier
   the peer references in its matching auth-response.

 - auth-response: opens a stream carrying the response body for a
   previously-issued auth-challenge identified by the same
   challenge_id. Same wire shape as auth-challenge.

Each instruction's body rides as blobs on the announced stream,
terminated by end. No paired pipe, no single-slot per-user buffering;
per-stream blob and end handlers do the work.

Also adds a destructor callback to guac_stream, called from
guac_user_free_stream / guac_client_free_stream and from
guac_user_free / guac_client_free for streams still open at disconnect,
so consumers can attach per-stream state that needs cleanup if the peer
drops mid-stream.

The first consumer is WebAuthn passthrough (companion PR on
guacamole-client) using application/x-webauthn-create+json and
application/x-webauthn-get+json mimetypes.
@aleitner aleitner force-pushed the GUACAMOLE-2281-webauthn-passthrough branch from 3a1ec95 to 64d2079 Compare June 17, 2026 04:16
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.

5 participants