-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
I plan on keeping this in this repository, rather than in the docs repo, for maintenance reasons.
- Loading branch information
Showing
4 changed files
with
354 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,47 +1,147 @@ | ||
/** Directory containing the qrexec sockets */ | ||
extern const char *socket_dir; | ||
|
||
/** | ||
* Connect to the listening socket for a Xen VM. | ||
* | ||
* \param domid The Xen domain ID of the VM. | ||
* \return The file descriptor for the socket. | ||
*/ | ||
__attribute__((warn_unused_result)) | ||
int connect_unix_socket_by_id(unsigned int domid); | ||
/** | ||
* Connect to the listening socket for a Xen VM. | ||
* | ||
* \param domname The name of the VM. | ||
* \return The file descriptor of the connection. | ||
*/ | ||
__attribute__((warn_unused_result)) | ||
int connect_unix_socket(const char *domname); | ||
/** | ||
* Handshake with the qrexec daemon connected on this FD. | ||
* | ||
* @param fd The file descriptor of the connection. | ||
* @return 0 on success or -1 on failure. | ||
*/ | ||
__attribute__((warn_unused_result)) | ||
int handle_daemon_handshake(int fd); | ||
/** | ||
* Send a message of type TYPE to the qrexec daemon connected on FD s. | ||
* | ||
* @param s The file descriptor for the connection. | ||
* @param other_domid The domain ID of the peer. | ||
* @param type The type of the message. | ||
* @param cmdline_param The parameter passed after the data. | ||
* @param cmdline_size The size of the cmdline_param passed. | ||
* @param[out] data_domain The domain ID to use for the data connection. | ||
* @param[out] data_port The port to use for the data connection. | ||
* @return true on success, false on failure. | ||
*/ | ||
__attribute__((warn_unused_result)) | ||
bool negotiate_connection_params(int s, int other_domid, unsigned type, | ||
const void *cmdline_param, int cmdline_size, | ||
int *data_domain, int *data_port); | ||
|
||
/** | ||
* Send a MSG_SERVICE_CONNECT to the daemon connected via file descriptor s. | ||
* | ||
* @param s The file descriptor for the connection. | ||
* @param connect_domain The domain ID of the calling domain. | ||
* @param connect_port The port to use for the data connection. | ||
* @return true on success, false on failure. | ||
*/ | ||
__attribute__((warn_unused_result)) | ||
bool send_service_connect(int s, const char *conn_ident, | ||
int connect_domain, int connect_port); | ||
|
||
/** | ||
* Run a qrexec command in dom0. | ||
* | ||
* @param svc_params The parameters of the service. | ||
* @param src_domain_id The source domain ID. | ||
* @param src_domain_name The source domain name. | ||
* @param cmd The command to execute. | ||
* @param connection_timeout The connection timeout in seconds. | ||
* @param exit_with_code \true if the return value should be the exit | ||
* code of the qrexec command, \false if the return value should be 0 | ||
* unless the command could not be executed for some reason. | ||
*/ | ||
__attribute__((warn_unused_result)) | ||
int run_qrexec_to_dom0(const struct service_params *svc_params, | ||
int src_domain_id, | ||
const char *src_domain_name, | ||
char *remote_cmdline, | ||
int connection_timeout, | ||
bool exit_with_code); | ||
/** Parameters for handshake_and_go(), organized as a struct | ||
* for convenience. */ | ||
struct handshake_params { | ||
/// Data vchan. | ||
libvchan_t *data_vchan; | ||
/// Buffer with data to be prepended to stdin. | ||
struct buffer *stdin_buffer; | ||
union { | ||
/// Return value of the preparation code: nonzero if there was already a problem. | ||
/// If this is nonzero the handshake is not run. | ||
int prepare_ret; | ||
/// Data protocol version from the handshake. | ||
int data_protocol_version; | ||
}; | ||
/// Whether this is a call to dom0 (true) or a call from dom0 (false). | ||
/// The name comes from the source of the call always sending the first | ||
/// handshake message. | ||
bool remote_send_first; | ||
/// Whether to return with status 0 or the return value of the call. | ||
bool exit_with_code; | ||
// whether qrexec-client should replace problematic bytes with _ before printing the output | ||
/// Whether to replace problematic bytes with _ before writing to stdout. | ||
bool replace_chars_stdout; | ||
/// Whether to replace problematic bytes with _ before writing to stderr. | ||
bool replace_chars_stderr; | ||
}; | ||
/** | ||
* Process IO call with the parameters specified by the parameters. | ||
* The vchan will be closed afterwards and set to NULL. | ||
* | ||
* \param params The parameters to use. | ||
* \param cmd The parsed command. | ||
*/ | ||
__attribute__((warn_unused_result)) | ||
int handshake_and_go(struct handshake_params *params, | ||
const struct qrexec_parsed_command *cmd); | ||
/** | ||
* Handshake with the remote qrexec-agent. | ||
* | ||
* \param vchan The vchan to use. | ||
* \param remote_send_first \true if the remote should send the first message, | ||
* otherwise \false. | ||
* \return The protocol version. Guaranteed to be either -1 (failure) or | ||
* between `QREXEC_PROTOCOL_V2` and `QREXEC_PROTOCOL_V3` inclusive. | ||
*/ | ||
__attribute__((warn_unused_result)) | ||
int handle_agent_handshake(libvchan_t *vchan, bool remote_send_first); | ||
/** | ||
* Execute the given qrexec command in dom0. If it requires data to be prepended | ||
* to its stdin, add that to the buffer. | ||
*/ | ||
__attribute__((warn_unused_result)) | ||
int prepare_local_fds(struct qrexec_parsed_command *command, struct buffer *stdin_buffer); | ||
|
||
/** | ||
* Execute the given command (of length service_length) in VM target. | ||
* | ||
* \param target The target VM name. | ||
* \param autostart \true to start the VM if it is not already started, otherwise \false. | ||
* \param remote_domain_id Xen domain ID of the remote domain. | ||
* \param cmd The command. | ||
* \param service_length The length of the command. | ||
* \param request_id The request ID used. | ||
* \param just_exec True for `MSG_JUST_EXEC`, false for `MSG_EXEC_CMDLINE`. | ||
* \param wait_connection_end \true to wait until the connection has finishe, else \false. | ||
* \return \true on success and \false on failure. | ||
*/ | ||
__attribute__((warn_unused_result)) | ||
bool qrexec_execute_vm(const char *target, bool autostart, int remote_domain_id, | ||
const char *cmd, size_t service_length, const char *request_id, | ||
bool just_exec, bool wait_connection_end); | ||
/* FD for stdout of remote process */ | ||
/** FD for stdout of remote process */ | ||
extern int local_stdin_fd; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
Qubes RPC Command Syntax and Execution | ||
====================================== | ||
|
||
Qubes RPC Command Syntax | ||
------------------------ | ||
|
||
Qubes RPC commands targeted at VMs use the following syntax: | ||
|
||
USERNAME:[nogui:]QUBESRPC SERVICE+ARGUMENT SOURCE_DOMAIN | ||
|
||
Qubes RPC commands targeted at the host use the following syntax: | ||
|
||
[nogui:]QUBESRPC SERVICE+ARGUMENT SOURCE_DOMAIN REQUESTED_TARGET_TYPE REQUESTED_TARGET | ||
|
||
Commands are generally considered trusted. There is some validation at | ||
various points, but this is haphazard at best. Instead, it is the job | ||
of the code that constructs a command to ensure that only valid commands | ||
are produced. For VM -> VM and VM -> dom0 calls, the command is constructed | ||
by qrexec-daemon (for Qubes OS R4.2 and up) or the policy engine (R4.1 and | ||
below). For dom0 -> VM calls, ensuring correctness is entirely the responsibility | ||
of the code that calls qrexec-client. | ||
|
||
qrexec-daemon assumes (but does not check) that: | ||
|
||
- The VM name passed on its command line is not empty and does not contain a space. | ||
- For calls to dom0, the requested target from the policy engine is not empty, | ||
does not contain a space, and is not the literal string ``@``. | ||
- Neither the default username passed on its command line nor the username | ||
from the policy engine contain a colon. | ||
|
||
If any of these assumptions are violated, qrexec-daemon will produce commands | ||
that will not be parsed correctly. In addition, its serialization of the | ||
request to the policy engine assumes the target VM name on its command line does | ||
not contain a newline. These are only minor bugs, not security vulnerabilities, | ||
because the inputs -- the policy engine and qubesd -- are ultimately trusted. | ||
|
||
Commands sent to a VM are submitted as ``MSG_JUST_EXEC`` or ``MSG_EXEC_CMDLINE`` | ||
messages to the VM's ``qrexec-daemon``'s listening socket. ``qrexec-daemon`` | ||
preprocesses these commands before sending them to the VM's ``qrexec-agent``. | ||
This preprocessing checks that the command is NUL-terminated and refuses | ||
the call otherwise. Furthermore, if the command starts with ``DEFAULT:``, | ||
``DEFAULT`` is replaced with the VM's default username. | ||
|
||
The parsing algorithm is the following: | ||
|
||
0. The command must contain a trailing NUL byte and have no other NUL characters. | ||
It is erroneous for the host to submit a command that violates this rule, but | ||
such violations are not required to be detected. They result in undefined | ||
behavior. | ||
|
||
1. In VMs only: find the first colon (``:``) in the command. If there is no colon, | ||
this is an error. Set the username to be everything before the colon. Remove | ||
the username and colon from the command. | ||
|
||
If the username is the literal string ``DEFAULT``, ``qrexec-daemon`` will replace | ||
it with its configured default user before transmitting the command to the VM. | ||
|
||
2. If the command starts with the literal string ``nogui:``, the command | ||
is not guaranteed to be able to show a GUI. The ``nogui:`` prefix is stripped | ||
before further processing. | ||
|
||
The meaning of this is implementation dependent. For Linux, it causes | ||
``wait-for-session=true`` in the service configuration file to be ignored. | ||
For Windows, it means that the command is run in session 0 as if it were | ||
a service, rather than in an interactive desktop session. | ||
|
||
The ``nogui:`` prefix is soft-deprecated. It will never be included in | ||
VM -> VM commands, only commands made via ``qvm-run`` from dom0 using | ||
``--no-gui``. Instead, the execution environment of a service should | ||
be determined by service configuration. This is possible on Linux, | ||
but it is not possible on Windows due to `a limitation in the Windows agent`_. | ||
A proposal to allow specifying ``nogui:`` via qrexec policy `was rejected`_. | ||
|
||
.. _was rejected: https://github.com/QubesOS/qubes-issues/issues/9180 | ||
.. _a limitation in the Windows agent: https://github.com/QubesOS/qubes-issues/issues/9198 | ||
|
||
3. If the command does not start with the literal string ``QUBESRPC``, | ||
it is treated as a shell command and further processing is skipped. | ||
This is mostly a legacy behavior and is only permitted for calls from | ||
the host. | ||
|
||
4. If the next character is not a space (ASCII 0x20), the results are | ||
undefined. The Linux agent fails the service call in this case. | ||
|
||
5. The ``QUBESRPC`` prefix and subsequent space are stripped. The rest of the | ||
command, including the terminating NUL byte, is the *service descriptor*. | ||
|
||
6. The service descriptor is split into tokens. Tokens are separated by a | ||
single space. If there are two or more consecutive spaces, or a trailing | ||
space, the results are undefined. The Linux agent fails the service call | ||
if there are two or more consecutive spaces before the second token has been | ||
parsed. | ||
|
||
7. If there are less than two tokens, this is an error and the call fails. | ||
Executable service (see below) check that there are exactly two or four | ||
tokens, but socket services do not. | ||
|
||
8. The tokens that results are assigned meanings as follows: | ||
|
||
1. The first token has the format ``SERVICE+ARGUMENT``. The *service name* is | ||
everything before the first ``+``, or the entire first argument if | ||
there is no ``+``. The *service argument* is everything after the first | ||
``+``. If the first argument does not contain ``+``, there is no service | ||
argument. | ||
|
||
2. The second token is the *source VM name*. | ||
|
||
3. The third token, if present, is the *requested target type*. | ||
It is ``name`` if the request was to a specific named VM, | ||
or ``keyword`` if the request was to a keyword. | ||
|
||
4. The fourth token, if present, is the *requested target*. | ||
It is the target requested by the VM normalized by the policy engine. | ||
For instance, nonexistent VM names have been replaced by ``@default``. | ||
If this starts with an ``@``, it is removed and the third token is | ||
``keyword``. | ||
|
||
Allowed keywords in this context are: | ||
|
||
- ``adminvm``: the Administrative VM, dom0. | ||
- ``default``: Default VM, lets qrexec policy choose. | ||
- ``dispvm``: Freshly created disposable VM based on source VM's default Disposable VM template. | ||
- ``dispvm:VMNAME``: Freshly create disposable VM based on the specified VM. | ||
|
||
Device model stubdomains (which have names ending in ``-dm``) *are* valid | ||
targets for qrexec requests, but *only* if they have a qrexec daemon running, | ||
and *only* if the call is made from dom0. The policy engine, and therefore | ||
all VM-initiated requests, act as if stubdomains do not exist. | ||
|
||
Tokens 3 and 4 are *always* present for calls to the host and *never* present otherwise. | ||
This is not checked, though. | ||
|
||
The use of a command string for calls to the host is an implementation detail. | ||
A future implementation of qrexec might avoid creating the command string at all, | ||
as it serves no function other than as an intermediate representation. After | ||
v4.2.19, it does not even leave the ``qrexec-daemon`` process. In the future, | ||
an array of strings should be used instead. | ||
|
||
Qubes RPC Call Flow | ||
------------------- | ||
|
||
In R4.1, the qrexec policy engine invokes ``qrexec-client``, which submits | ||
the command to a ``qrexec-daemon`` for execution or runs it on the host. In R4.2 before | ||
v4.2.19, ``qrexec-daemon`` uses the result of the policy evaluation to call ``qrexec-client`` | ||
itself. In v4.2.19 and above, ``qrexec-daemon`` directly submits or executes the call, | ||
and ``qrexec-client`` is only used for calls submitted by dom0. | ||
|
||
Qubes RPC service execution | ||
--------------------------- | ||
|
||
A Qubes RPC service is a file, socket, or symbolic link under ``/etc/qubes-rpc/`` | ||
or ``/usr/local/etc/qubes-rpc``. When a call is made, ``qrexec-agent`` (in a VM) | ||
or ``qrexec-client`` (in dom0) searches for the first entry in the following list: | ||
|
||
1. ``/usr/local/etc/qubes-rpc/SERVICE+ARG`` | ||
2. ``/etc/qubes-rpc/SERVICE+ARG`` | ||
3. ``/usr/local/etc/qubes-rpc/SERVICE`` | ||
4. ``/etc/qubes-rpc/SERVICE`` | ||
|
||
``SERVICE`` is replaced by the service being invoked, and ``ARG`` by its argument. | ||
If ``SERVICE`` is longer than ``NAME_MAX`` (255 on Linux), the call fails. If | ||
``SERVICE`` is ``NAME_MAX`` or less, but ``SERVICE+ARG`` exceeds ``NAME_MAX``, | ||
steps 1 and 3 are skipped. If no argument at all is provided, the search proceeds | ||
as if an empty argument is passed. This is only possible for calls made by dom0, | ||
as VM => VM calls insert an empty argument if no argument is provided. | ||
|
||
The search is terminated by any of the following: | ||
|
||
1. There are no more paths to check. This causes the search to fail. | ||
2. ``lstat(2)`` fails, setting errno to a value other than ``ENOENT``. | ||
This causes the search to fail. | ||
3. ``lstat(2)`` succeeds. The search concludes successfully. | ||
|
||
If a command is not found, qrexec pretends that it exited with status 127. | ||
If a command cannot be executed, qrexec pretends that it exited with status 125. | ||
In both cases, no data is read from stdin, and no data is written to stdout or | ||
stderr. However, the actual cause of the failure is logged within the VM. | ||
|
||
Symbolic links are followed when executing a service. However, it is usually | ||
a mistake to use program as a qrexec service that was not intended for this use, | ||
such as ``/usr/bin/cat``. This is because ``-`` is allowed to be the first | ||
character in the service argument, allowing option injection attacks. | ||
Instead, a wrapper script should be used. | ||
|
||
Types of qrexec services | ||
------------------------ | ||
|
||
There are three different types of qrexec services. The distinction | ||
between service types is mostly invisible to callers. | ||
|
||
1. Executable services. These are files with execute permission, as | ||
reported by ``euidaccess(2)``. They are executed using ``execve()``. | ||
In a VM (but not in dom0), they are executed in a proper login session. | ||
On Linux, PAM is used. | ||
|
||
By default, these will run as the user passed by dom0. This can be overridden | ||
with ``force-user=`` in the configuration file. The username must be a string | ||
user due to PAM limitations. | ||
|
||
2. Socket-based services. These are ``AF_UNIX`` stream sockets on the filesystem. | ||
Data passed via stdin will be written to the socket, and data from the socket will | ||
will be written to stdout. | ||
|
||
By default, qrexec will write the service descriptor before it writes any data | ||
from the peer. This can be disabled with ``skip-service-descriptor=true`` | ||
in the configuration file. The username is *not* sent to the socket. | ||
|
||
The connection to the service is always made as *root* or as the *default user*. | ||
Which one is used is unspecified, and services should not rely on this. Instead, | ||
the socket should be owned by ``root:qubes`` with ``0660`` permissions. | ||
|
||
3. Symlinks to ``/dev/tcp/``, optionally followed by a hostname and a port number. | ||
The allowed formats of the symlink target are:: | ||
|
||
/dev/tcp/HOST/PORT | ||
/dev/tcp/HOST | ||
/dev/tcp | ||
|
||
The first syntax ignores the service argument. The second syntax | ||
treats the entire service argument as the port, and the third syntax | ||
splits the service argument (on the last ``+``) to obtain both the host | ||
and the port. | ||
|
||
The port must be a decimal integer with no leading zeros. This is checked: | ||
the call will fail if it is not. | ||
|
||
The host may be either an IPv4 or IPv6 address. If it contains ``:`` or ``%``, it | ||
is checked to be an IPv6 address by setting :code:`.ai_family = AF_INET6`. | ||
Otherwise, it may be either an IPv4 or IPv6 address. In a service call | ||
argument, ``:`` must be encoded as ``+`` and ``%`` is not allowed. ``AI_NUMERICHOST`` | ||
is always set, so hostnames are not allowed. | ||
|
||
TCP socket services are checked for before executable or socket-based services, so | ||
a symlink to a service under ``/dev/tcp/`` will be interpreted as a TCP socket service. | ||
This is not expected to be an issue in practice, because ``/dev/tcp/`` does not exist | ||
on any common \*nix variant. | ||
|
||
Service descriptors are still sent to TCP socket services by default. If a TCP socket | ||
service is used for a service that is not Qubes OS-aware, ``skip-service-descriptor = true`` | ||
should be used in the configuration file. |
Oops, something went wrong.