Skip to content

Commit

Permalink
Wayland in QM post
Browse files Browse the repository at this point in the history
Signed-off-by: Albert Esteve <aesteve@redhat.com>
  • Loading branch information
aesteve-rh committed Jul 19, 2024
1 parent d48ec04 commit 57cea36
Show file tree
Hide file tree
Showing 2 changed files with 396 additions and 0 deletions.
396 changes: 396 additions & 0 deletions content/posts/wayland-in-qm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,396 @@
---
title: "Wayland in the QM Container"
date: 2024-07-19T09:33:24+02:00
draft: true
---

Lately we have been working, with Roberto Majadas, in designing a
scenario capable of running a [Wayland](https://wayland.freedesktop.org/)
compositor in the [QM Container](https://github.com/containers/qm).
The required bits to support this scenario in the QM have been recently
upstreamed, and will be available in the next release (>=0.6.6). This post
explains the process and all the configuration required to make it work.

## Creating a new session

First of all, a Wayland compositor requires an active session and a
seat (i.e., `seat0`). The seat contains devices (e.g., `/dev/dri/card0`) that
the compositor will need to access. Furthermore, the session needs a
TTY associated (typically, TTY7, since it provides a graphical environmnet).

Since we want the session to start automatically when the QM container
starts, we will employ systemd to create the session.

- Create a new service file at `/etc/systemd/system/wayland-session.service`:

```ini
[Unit]
Description=Wayland Session Creation Handling
After=systemd-user-sessions.service

[Service]
Type=simple
Environment=XDG_SESSION_TYPE=wayland
UnsetEnvironment=TERM
ExecStart=/bin/sleep infinity
Restart=no

# Run the session as root (required by PAMName)
User=0
Group=0

# Set up a full user session for the user, required by Wayland.
PAMName=login

# Fail to start if not controlling the tty.
StandardInput=tty-fail

# Defaults to journal.
StandardError=journal
StandardOutput=journal

# A virtual terminal is needed.
TTYPath=/dev/tty7
TTYReset=yes
TTYVHangup=yes
TTYVTDisallocate=yes

# Log this user with utmp.
UtmpIdentifier=tty7
UtmpMode=user

[Install]
WantedBy=graphical.target
```

A few remarks:

*`After=systemd-user-sessions.service`*: To make sure service is started after
logins are permitted.

*`PAMName`*: Sets the PAM service name to set up a session as. Requires *`User=`*
or else the setting is ignored.

*`ExecStart=/bin/sleep infinity`*: We want to keep the service active as long
as the QM container is running. However, we do not want to execute the
compositor directly from the service, as it should be run as a container.
On the other hand, this can not be a [Quadlet](https://www.redhat.com/sysadmin/quadlet-podman)
either, since we want the *`Type=simple`* or the session will not start.
As a consequence, we just let it sleep.

- Make sure the service is enabled:

```shell
$ systemctl enable wayland-session
```

So that looks good enough, but unfortunately will not work. We are logging in
as root, which is mapped to the
[SELinux](https://www.redhat.com/en/topics/linux/what-is-selinux)' root user.
As part of the PAM login process, SELinux tries to perform a context switch,
which falls back to an `unconfined_u:unconfined_r:unconfined_t:s0` target
context. QM fobids the context switch, as it is unsafe, and the login fails.

The problem is within the PAM configuration files, at `/etc/pam.d/*`.
Specifically, we are running the login service at `/etc/pam.d/login` (given
by the `PAMName` setting of the service file), which includes a `pam_selinux`
step performing the context switch based on the user mappings. We want to
avoid any context switch, so that the resulting logged user stays in the
safe `qm_t` context (check `id -Z`).

- To do this and keep the login configuration as is (to avoid other attemps to login),
let's create a new configuration file at `/etc/pam.d/wayland`:

```
#%PAM-1.0
auth substack system-auth
auth include postlogin
account required pam_nologin.so
account include system-auth
password include system-auth
session required pam_loginuid.so
session required pam_namespace.so
session optional pam_keyinit.so force revoke
session include system-auth
session include postlogin
-session optional pam_ck_connector.so
```

And we change the PAMName to `wayland` in our `wayland-session.service` file.
Is that good enough? Well, not really. As part of the login process, the
user@ service is launched (i.e., `user@0.service`), which executes
`/usr/lib/systemd/systemd --user`, and another context switch is attempted and
fails. There is no way around it this time, we need to explicitely modify
the system configuration.

- Edit file at `/etc/pam.d/systemd-user` and remove/comment the lines
containing `pam_selinux`, resulting in something similar to this:

```
# This file is part of systemd.
#
# Used by systemd --user instances.
account sufficient pam_unix.so no_pass_expiry
account include system-auth
session required pam_loginuid.so
session optional pam_keyinit.so force revoke
session optional pam_umask.so silent
session required pam_namespace.so
session include system-auth
```

If we start the container now, we should be able to see the session available:

```bash
bash-5.1# loginctl
SESSION UID USER SEAT TTY STATE IDLE SINCE
1 0 root seat0 tty7 online no

1 sessions listed.
```

However, the session is not active yet. In order to active the session
automatically before the compositor starts, we need to run `loginctl active`.

- Use a Quadlet file at `/etc/containers/systemd/session-activate.container`
to run the command inside a container:

```ini
[Unit]
Description=session-activate container

[Container]
ContainerName=session-activate
Environment=XDG_RUNTIME_DIR=/run/user/0
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket
Exec=/usr/bin/entrypoint.sh
Image=session-activate:latest
SecurityLabelType=qm_container_wayland_t
Volume=/run/systemd:/run/systemd:ro
Volume=/run/dbus/system_bus_socket:/run/dbus/system_bus_socket
Volume=/run/user/0:/run/user/0

[Install]
WantedBy=multi-user.target

[Service]
Restart=always
```

Notice the relabel to `qm_container_wayland_t`. This SELinux type must be
used by all containers running a Wayland application. We will see this label
used in other Quadlets in this guide. Otherwise the applications may lack
the required permissions to perform their tasks.

I will omit the `Containerfile` for this and other containers in this guide,
they are not too important and relatively simple in general.
In this case, however, it is worth showing what the `entrypoint.sh` script
looks like:

```bash
#!/bin/bash

SESSION=
while [ -z "$SESSION" ]; do
sleep 1
SESSION=$(loginctl list-sessions -o json | jq -re '.[] | select(.seat=="seat0").session')
done

loginctl activate $SESSION

exit 0
```

This changes the state of the session to active. But one more thing is required.
We explained how to create and activate a new session for `root`. However,
we need to create a user directory at `/run/user/0`, or else the
`session-activate.service` will fail to mount the volume.

- Create a new tmpfile drop-in configuration at `/usr/lib/tmpfiles.d/wayland-xdg-directory.conf`:
```
#Type Path Mode UID GID Age Argument
d /run/user/0 0700 0 0 - -
```

And this finishes the first step toward our goal.

## Start a user DBus daemon

This may be considered optional, but recommended. In order to avoid any process
to flood the system bus socket, which may jeopardize system stability, we are
going to create a new bus socket for the compositor. This can be done in a
similarly as how the system socket is created.

- Create a systemd socket file at `/etc/systemd/system/qm-dbus.socket`:

```ini
[Unit]
Description=QM D-Bus User Message Bus Socket
After=dbus.socket

[Socket]
ListenStream=%t/dbus/qm_bus_socket

[Install]
WantedBy=sockets.target
```

Make sure the systemd socket is enabled:

```shell
$ systemctl enable qm-dbus.socket
```

This creates the socket at `/run/dbus/qm_bus_socket`.

- Run the DBus daemon with a Quadlet file at `/etc/containers/systemd/qm-dbus-broker.container`:

```ini
[Unit]
After=qm-dbus.socket
Description=qm-dbus-broker container
Requires=qm-dbus.socket

[Container]
ContainerName=qm-dbus-broker
Environment=XDG_RUNTIME_DIR=/run/user/0
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/dbus/qm_bus_socket
Exec=/usr/bin/dbus-broker-launch --scope user
Image=qm-dbus-broker:latest
SecurityLabelType=qm_container_wayland_t
Volume=/run/dbus/qm_bus_socket:/run/dbus/qm_bus_socket
Volume=/run/systemd:/run/systemd:ro
Volume=/run/user/0:/run/user/0
Volume=/etc/machine-id:/etc/machine-id:ro

[Install]
Alias=qm-dbus.service
WantedBy=multi-user.target

[Service]
Restart=always
Sockets=qm-dbus.socket
```

## Launching Wayland container

For this section we are going to assume [Mutter](https://mutter.gnome.org/),
but similar steps can be used for other compositors.

First of all, we need to mount all required devices in the QM container.

- To this end, place a drop-in configuration file at
`/etc/containers/systemd/qm.container.d/wayland-extra-devices.conf` in the
ASIL layer:

```ini
[Container]
AddDevice=/dev/dri/renderD128
AddDevice=/dev/dri/card0
AddDevice=/dev/tty0
AddDevice=/dev/tty1
AddDevice=/dev/tty2
AddDevice=/dev/tty3
AddDevice=/dev/tty4
AddDevice=/dev/tty5
AddDevice=/dev/tty6
AddDevice=/dev/tty7
AddDevice=/dev/input/event0
AddDevice=/dev/input/event1
AddDevice=/dev/input/event2
AddDevice=/dev/input/event3
AddDevice=/dev/input/event4
Volume=/run/udev:/run/udev:ro,Z
```

- We can now just create a new Quadlet file for Mutter:

```ini
[Unit]
After=qm-dbus.socket
Description=mutter container
Requires=qm-dbus.socket

[Container]
ContainerName=mutter
Environment=XDG_RUNTIME_DIR=/run/user/0
Environment=XDG_SESSION_TYPE=wayland
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/dbus/qm_bus_socket
Exec=mutter --no-x11 --wayland --sm-disable --wayland-display=wayland-0
Image=mutter:latest
SecurityLabelType=qm_container_wayland_t
Volume=/run/systemd:/run/systemd:ro
Volume=/run/udev:/run/udev:ro
Volume=/run/dbus/qm_bus_socket:/run/dbus/qm_bus_socket
Volume=/run/dbus/system_bus_socket:/run/dbus/system_bus_socket
Volume=/run/user/0:/run/user/0
AddDevice=/dev/dri/renderD128
AddDevice=/dev/dri/card0
AddDevice=/dev/tty0
AddDevice=/dev/tty1
AddDevice=/dev/tty2
AddDevice=/dev/tty3
AddDevice=/dev/tty4
AddDevice=/dev/tty5
AddDevice=/dev/tty6
AddDevice=/dev/tty7
AddDevice=/dev/input/event0
AddDevice=/dev/input/event1
AddDevice=/dev/input/event2
AddDevice=/dev/input/event3
AddDevice=/dev/input/event4

[Install]
WantedBy=multi-user.target

[Service]
Restart=always
```

This should get the Mutter compositor running inside the QM container!

However, the screen is empty, with a nice blue background. Now we can finally
add GUI applications that we want to be composited on the screen.

## Painting applications on screen

We will try to make a weston terminal appear in the screen.
This is just an example to illustrate the configuration.

After all we did so far, this should be easy enough in comparison. Just
take into account that workloads need to be containerized and relabeled
to `qm_container_wayland_t`.

- Quadlet configuration file at `/etc/containers/systemd/weston_terminal.container`
would look like this:

```ini
[Unit]
After=mutter.service
Description=weston_terminal container
Requires=mutter.service

[Container]
ContainerName=weston_terminal
Environment=XDG_RUNTIME_DIR=/run/user/0
Environment=WAYLAND_DISPLAY=wayland-0
Exec=/usr/bin/weston-terminal
Image=localhost/weston_terminal:latest
SecurityLabelType=qm_container_wayland_t
Volume=/run/user/0:/run/user/0

[Install]
WantedBy=multi-user.target

[Service]
Restart=always
```

If everything went well, you can now restart the QM container,
and the terminal will appear with the familiar Mutter background:

![mutter-in-qm](/img/mutter.png)

Enjoy!
Binary file added static/img/mutter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 57cea36

Please sign in to comment.