talex5 / wayland-proxy-virtwl

Allow guest VMs to open windows on the host
Apache License 2.0
103 stars 11 forks source link

A Wayland proxy

wayland-proxy-virtwl accepts Wayland connections from client applications and proxies the messages to a host compositor. Features:

It is similar to the sommelier proxy from ChromiumOS, but written in OCaml. It adds support for the primary selection, and aims to be easier to modify and less segfaulty. It uses the ocaml-wayland library.

It is able to proxy Evince and Firefox at least, and also works with Xwayland well enough to run gvim.

See Qubes-lite With KVM and Wayland for a setup using this.

Installation

To install using the Nix flake:

nix run 'git+https://github.com/talex5/wayland-proxy-virtwl.git?submodules=1'

To build from a Git clone:

git clone --recursive https://github.com/talex5/wayland-proxy-virtwl.git

Then build with either opam (opam install .) or Nix (nix build '.?submodules=1').

I use the following systemd file to run the proxy (in ~/.local/share/systemd/user/wayland-proxy-virtwl.service):

[Unit]
Description=Wayland-proxy-virtwl

[Service]
ExecStart=/path/to/wayland-proxy-virtwl --virtio-gpu --tag="[my-vm] " --wayland-display wayland-0 --x-display=0 --xrdb Xft.dpi:150

[Install]
WantedBy=default.target

virtio-gpu support

If --virtio-gpu is passed then, rather than connecting to a local Wayland compositor, the proxy will search /dev/dri/ for a virtio-gpu device and use that to connect to the compositor on the host.

Note: the proxy previously used the virtwl protocol, but virtio-gpu has now replaced it.

crosvm setup

You will need crosvm R123-15786 or later on the host. I use gitlab:talex5/crosvm, which has some important fixes applied (see the Git log for details). It can be run as a Nix flake like this:

nix run 'git+https://gitlab.com/talex5/crosvm.git?ref=b124-tal&submodules=1'

You will need to run with --gpu=context-types=cross-domain:virgl2 to proxy Wayland connections. My qubes-lite repository creates the scripts I use to run it, but these will need modifying for other systems.

Guest setup

Support for virtio-gpu contexts was added in Linux 5.16. You will need to load (or compile in) the virtio_gpu kernel module. On success, you should see these kernel messages (with +context_init):

[drm] features: +virgl -edid +resource_blob +host_visible
[drm] features: +context_init

You should find you now have a /dev/dri directory with some device files (and maybe some debug files in /sys/kernel/debug/dri).

Guest acceleration

Since regular guest memory cannot be shared with the host, the proxy allocates a shadow buffer from the host and copies the frame data into that. Wayland can also use graphics memory directly, which should avoid the copy, but this is not yet supported.

Xwayland support

If you run with --x-display=0 then it listens on the abstract socket @/tmp/.X11-unix/X0 for X11 clients. If one tries to connect, it spawns an Xwayland process to handle it (and any future X11 clients).

Xwayland is an X server that renders application window contents to Wayland surfaces. wayland-proxy-virtwl acts as a window manager to integrate these surfaces into the Wayland desktop (e.g. by reading the WM_NAME X11 property and setting it as the xdg_toplevel's title).

This is rather complicated. The following features mostly work:

Xwayland doesn't support Wayland's HiDPI feature, which causes the compositor to double everything in size, which is often blurry and unusable. To fix this, use e.g. --x-unscale=2 to reverse this transformation (and then just configure the X11 apps to use a larger font).

You can use the --xrdb 'KEY:VALUE' option to set default settings in the xrdb database. For example, --xrdb Xft.dpi:150 is useful on high-DPI screens.

Limitations:

For more details, see Isolating Xwayland in a VM.

Socket activation

systemd-style socket activation is supported. Sockets named "wayland" and/or "x11" may be provided, and the proxy will use them instead of creating its own. If a wayland socket is provided, the --wayland-display option cannot be used. For X11, the --x-display option is still needed, however.

Logging

There are several ways to enable logging:

The available log sources are:

You can also suppress certain classes of log messages using e.g. --log-suppress motion,shm,delete,region,drawing,hints. The classes are:

See trace.ml for details.

You can configure the proxy to log to an in-memory ring-buffer, and then dump that whenever an error occurs. Use e.g. -v --log-ring-path ~/wayland.log to enable this feature. When an uncaught exception occurs, the proxy will flush (append) the log to the given path. By default, it keeps about 512K of history; use --log-ring-size to change this.

You can also force it to flush the log by writing a line to the control pipe. e.g.

wayland-proxy-virtwl ... -v --log-ring-path ~/wayland.log &
echo dump-log > /run/user/1000/wayland-1-ctl
cat ~/wayland.log

This is useful if an application is misbehaving and you want to check its recent interactions.

Hacking

Execution starts in main.ml, which parses the command-line arguments and then starts listening for Wayland and/or X connections.

Wayland connections are handled by relay.ml. Relay.create connects to the host compositor and Relay.accept accepts the connection from the client (each connection gets its own relay anyway, but this split simplifies the code a bit). make_registry generates the Wl_registry object offered to clients. When a client binds to one of the offered APIs, the on_bind method binds the corresponding host API and calls an API-specific function to relay messages between the new client-side object and the new host object.

There is one function for each Wayland interface. For example, the Xdg_popup interface is handled like this:

let make_popup ~host_popup c =
  let h = host_popup @@ object
      inherit [_] H.Xdg_popup.v1
      method on_popup_done _ = C.Xdg_popup.popup_done c
      method on_configure _ = C.Xdg_popup.configure c
      method on_repositioned _ = C.Xdg_popup.repositioned c
    end
  in
  Proxy.Handler.attach c @@ object
    inherit [_] C.Xdg_popup.v1
    method on_destroy = delete_with H.Xdg_popup.destroy h
    method on_grab _ ~seat = H.Xdg_popup.grab h ~seat:(to_host seat)
    method on_reposition _ ~positioner = H.Xdg_popup.reposition h ~positioner:(to_host positioner)
  end

The host_popup function will create the host object, once you provide a set of event handlers for it. c is the corresponding client-side object. Whenever you get an event from the host, send the same event to the client. The client-side APIs are accessible via the C module, and the host-side ones via H.

Then Handler.attach c sets up handlers for requests from the client. When you get a request, make the corresponding request on the host object. on_destroy can be handled using delete_with so that the generated delete_id event gets relayed too.

If the trailing arguments are the same, they are often omitted. For example, on_configure above could have been written out in full as:

  let h = host_popup @@ object
      inherit [_] H.Xdg_popup.v1
      [...]
      method on_configure _ ~x ~y ~width ~height =
        C.Xdg_popup.configure c ~x ~y ~width ~height

If an argument is another object, you will need to convert it. For example, when the client makes a grab request they pass a client-side Wl_seat object. When calling the host compositor, we must pass the corresponding host-side object. to_host and to_client perform these conversions. Objects that require conversion in this way must provide a user_data method to get their peer.

Objects that need to do special things when running under virtio-gpu take an extra virtio_gpu option, which allows allocating image buffers on the host. Objects that interact with Xwayland take an xwayland option with hooks for that.

virtio-gpu cross-domain protocol

I couldn't find any documentation on the protocol, but virtio-spec.md contains my guesses about how it's supposed to work.

tests/test.ml is a simple test application that uses the virtio-gpu kernel interface directly (without the proxy), avoiding any copying.

Updating a protocol

The proxy can only relay messages it knows about. To add support for a newer version of protocol:

  1. Update the XML file in https://github.com/talex5/ocaml-wayland/tree/master/protocols.
  2. Build the proxy with the new version of ocaml-wayland, either by installing it with opam or by putting it inside the proxy's directory (note that the proxy might already contain a git-submodule of it; update that if so).
  3. The compiler errors will take you to any places that need updating.

Adding a new protocol

  1. Run your test application with WAYLAND_DEBUG=1 directly on the host to find out what protocol it needs.
  2. Add the XML file to ocaml-wayland's protocols directory, and extend the dune file to build it.
  3. Import it into c.ml, h.ml and protocols.ml. These just provide aliases so you can e.g. refer to a client Wl_surface as C.Wl_surface.
  4. Extend relay.ml's registry function: list the new interface and handle it in on_bind.
  5. You can mostly just follow the compiler errors to implement it, copying the pattern of the other objects.

If your interface needs to do things with virtio-gpu, it's probably easiest to get it working on the host, without virtio-gpu, first.

TODO