mirror of
https://github.com/QubesOS/qubes-doc.git
synced 2024-12-18 12:14:42 -05:00
302 lines
8.4 KiB
ReStructuredText
302 lines
8.4 KiB
ReStructuredText
=============================
|
||
Qrexec: socket-based services
|
||
=============================
|
||
|
||
|
||
*This page describes how to implement and use new socket-backed services for qrexec. See* :doc:`qrexec </developer/services/qrexec>` *for general overview of the qrexec framework.*
|
||
|
||
As of Qubes 4.1, qrexec allows implementing services not only as
|
||
executable files, but also as Unix sockets. This allows Qubes RPC
|
||
requests to be handled by a server running in a VM and listening for
|
||
connections.
|
||
|
||
How it works
|
||
------------
|
||
|
||
|
||
When a Qubes RPC service is invoked, qrexec searches for a file that
|
||
handles it in the qubes-rpc directories (``/etc/qubes-rpc`` or
|
||
``/usr/local/etc/qubes-rpc``). If the file is a Unix socket, qrexec will
|
||
try to connect to it.
|
||
|
||
Before passing user input, the socket service will receive a
|
||
null-terminated service descriptor, i.e. the part after ``QUBESRPC``.
|
||
When running in a VM, this is:
|
||
|
||
.. code:: bash
|
||
|
||
<service_name> <source>\0
|
||
|
||
|
||
|
||
When running in dom0, it is:
|
||
|
||
.. code:: bash
|
||
|
||
<service_name> <source> <target_type> <target>\0
|
||
|
||
|
||
|
||
(The target type can be ``name``, in which case target is a domain name,
|
||
or ``keyword``, in which the target is a keyword like ``@dispvm``).
|
||
|
||
Afterwards, data provided by the service’s user (as stdin) is sent into
|
||
the socket, and data received from the socket is sent back to the user
|
||
(as stdout). When the service closes the socket, an exit code of 0 is
|
||
sent back to the user.
|
||
|
||
Differences from executable-based services
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
|
||
From the user point of view, the socket-based service behaves almost
|
||
like an executable-based one. Here are the differences:
|
||
|
||
- There is no stderr (the socket provides only one output stream).
|
||
Currently, that means stderr will also never be closed on user’s end.
|
||
|
||
- There is no exit code. When the socket connection is closed, exit
|
||
code 0 is sent to the user.
|
||
|
||
|
||
|
||
Recommended use
|
||
---------------
|
||
|
||
|
||
Create a program that binds to path *outside* ``/etc/qubes-rpc``, such
|
||
as ``/var/run/my-daemon.sock``. Put a symlink in ``/etc/qubes-rpc``,
|
||
e.g. ``ln -s /var/run/my-daemon.sock /etc/qubes-rpc/qubes.Service``.
|
||
|
||
If your program handles multiple services, create multiple symlinks. You
|
||
can dispatch based on the service descriptor.
|
||
|
||
Do not run the program as root.
|
||
|
||
You can use systemd and socket activation so that the program is started
|
||
only when the service is invoked. See the below example.
|
||
|
||
Example: ``qrexec-policy-agent``
|
||
--------------------------------
|
||
|
||
|
||
``qrexec-policy-agent`` is the program that handles “ask” prompts for
|
||
Qubes RPC calls. It is a good example of an application that: * Uses
|
||
Python and asyncio. * Runs as a daemon, to save some overhead on
|
||
starting process. * Runs as a normal user. This is achieved using
|
||
user’s instance of systemd. * Uses systemd socket activation. This way
|
||
it can be installed in all VMs, but started only if it’s ever needed.
|
||
|
||
See the
|
||
`qubes-core-qrexec <https://github.com/QubesOS/qubes-core-qrexec/>`__
|
||
repository for details.
|
||
|
||
Systemd unit files
|
||
^^^^^^^^^^^^^^^^^^
|
||
|
||
|
||
**/lib/systemd/user/qubes-qrexec-policy-agent.service**: This is the
|
||
service configuration.
|
||
|
||
.. code:: bash
|
||
|
||
[Unit]
|
||
Description=Qubes remote exec policy agent
|
||
ConditionUser=!root
|
||
ConditionGroup=qubes
|
||
Requires=qubes-qrexec-policy-agent.socket
|
||
|
||
[Service]
|
||
Type=simple
|
||
ExecStart=/usr/bin/qrexec-policy-agent
|
||
|
||
[Install]
|
||
WantedBy=default.target
|
||
|
||
|
||
|
||
**/lib/systemd/user/qubes-qrexec-policy-agent.socket**: This is the
|
||
socket file that will activate the service.
|
||
|
||
.. code:: bash
|
||
|
||
[Unit]
|
||
Description=Qubes remote exec policy agent socket
|
||
ConditionUser=!root
|
||
ConditionGroup=qubes
|
||
PartOf=qubes-qrexec-policy-agent.service
|
||
|
||
[Socket]
|
||
ListenStream=/var/run/qubes/policy-agent.sock
|
||
|
||
[Install]
|
||
WantedBy=sockets.target
|
||
|
||
|
||
|
||
Note the ``ConditionUser`` and ``ConditionGroup`` that ensure that the
|
||
socket and service is started only as the right user
|
||
|
||
Start the socket using ``systemctl --user start``. Enable it using
|
||
``systemctl --user enable``, so that it starts automatically.
|
||
|
||
.. code:: bash
|
||
|
||
systemctl --user start qubes-qrexec-policy-agent.socket
|
||
systemctl --user enable qubes-qrexec-policy-agent.socket
|
||
|
||
|
||
|
||
Alternatively, you can enable the service by creating a symlink:
|
||
|
||
.. code:: bash
|
||
|
||
sudo ln -s /lib/systemd/user/qubes-qrexec-policy-agent.socket /lib/systemd/user/sockets.target.wants/
|
||
|
||
|
||
|
||
Link in qubes-rpc
|
||
^^^^^^^^^^^^^^^^^
|
||
|
||
|
||
``qrexec-policy-agent`` will handle a Qubes RPC service called
|
||
``policy.Ask``, so we add a link:
|
||
|
||
.. code:: bash
|
||
|
||
sudo ln -s /var/run/qubes/policy-agent.sock /etc/qubes-rpc/policy.Ask
|
||
|
||
|
||
|
||
Python server with socket activation
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
|
||
Socket activation in systemd works by starting our program with the
|
||
socket file already bound at a specific file descriptor. It’s a simple
|
||
mechanism based on a few environment variables, but the canonical way is
|
||
to use the ``sd_listen_fds()`` function from systemd library (or, in our
|
||
case, its Python version).
|
||
|
||
Install the Python systemd library:
|
||
|
||
.. code:: bash
|
||
|
||
sudo dnf install python3-systemd
|
||
|
||
|
||
|
||
Here is the server code:
|
||
|
||
.. code:: python
|
||
|
||
import os
|
||
import asyncio
|
||
import socket
|
||
|
||
from systemd.daemon import listen_fds
|
||
|
||
|
||
class SocketService:
|
||
def __init__(self, socket_path, socket_activated=False):
|
||
self._socket_path = socket_path
|
||
self._socket_activated = socket_activated
|
||
|
||
async def run(self):
|
||
server = await self.start()
|
||
async with server:
|
||
await server.serve_forever()
|
||
|
||
async def start(self):
|
||
if self._socket_activated:
|
||
fds = listen_fds()
|
||
if fds:
|
||
assert len(fds) == 1, 'too many listen_fds: {}'.format(
|
||
listen_fds)
|
||
sock = socket.socket(fileno=fds[0])
|
||
return await asyncio.start_unix_server(self._client_connected,
|
||
sock=sock)
|
||
|
||
if os.path.exists(self._socket_path):
|
||
os.unlink(self._socket_path)
|
||
return await asyncio.start_unix_server(self._client_connected,
|
||
path=self._socket_path)
|
||
|
||
async def _client_connected(self, reader, writer):
|
||
try:
|
||
data = await reader.read()
|
||
assert b'\0' in data, data
|
||
|
||
service_descriptor, data = data.split(b'\0', 1)
|
||
|
||
response = await self.handle_request(service_descriptor, data)
|
||
|
||
writer.write(response)
|
||
await writer.drain()
|
||
finally:
|
||
writer.close()
|
||
await writer.wait_closed()
|
||
|
||
async def handle_request(self, service_descriptor, data):
|
||
# process params, return response
|
||
|
||
return response
|
||
|
||
|
||
def main():
|
||
socket_path = '/var/run/qubes/policy-agent.sock'
|
||
service = SocketService(socket_path)
|
||
|
||
loop = asyncio.get_event_loop()
|
||
loop.run_until_complete(service.run())
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|
||
|
||
|
||
You can also use ``qrexec/server.py`` from
|
||
`qubes-core-qrexec <https://github.com/QubesOS/qubes-core-qrexec/>`__
|
||
repository, which is a variant of the above code - but note that
|
||
currently it’s somewhat more specific (JSON requests and ASCII
|
||
responses; no target handling in service descriptors).
|
||
|
||
Using the service
|
||
^^^^^^^^^^^^^^^^^
|
||
|
||
|
||
The service is invoked in the same way as a standard Qubes RPC service:
|
||
|
||
.. code:: bash
|
||
|
||
echo <input_data> | qrexec-client -d domX 'DEFAULT:QUBESRPC policy.Ask'
|
||
|
||
|
||
|
||
You can also connect to it locally, but remember to include the service
|
||
descriptor:
|
||
|
||
.. code:: bash
|
||
|
||
echo -e 'policy.Ask dom0\0<input data>' | nc -U /etc/qubes-rpc/policy.Ask
|
||
|
||
|
||
|
||
Further reading
|
||
---------------
|
||
|
||
|
||
- :doc:`Qrexec overview </developer/services/qrexec>`
|
||
|
||
- :doc:`Qrexec internals </developer/services/qrexec-internals>`
|
||
|
||
- `qubes-core-qrexec <https://github.com/QubesOS/qubes-core-qrexec/>`__
|
||
repository - contains the above example
|
||
|
||
- `systemd.socket <https://www.freedesktop.org/software/systemd/man/systemd.socket.html>`__
|
||
- socket unit configuration
|
||
|
||
- `Streams in Python asyncio <https://docs.python.org/3/library/asyncio-stream.html>`__
|
||
|
||
|