Wayland in the QM Container
Lately I have been working, with Roberto Majadas, in designing a scenario capable of running a Wayland compositor in the QM Container. 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
Firstly, 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 environment).
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
:
[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
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:
$ 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’s 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 attempts 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 containingpam_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-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 activate 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:
[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 relabeling 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:
#!/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 similarly as how the system socket is created.
- Create a systemd socket file at
/etc/systemd/system/qm-dbus.socket
:
[Unit]
Description=QM D-Bus User Message Bus Socket
After=dbus.socket
[Socket]
ListenStream=%t/dbus/qm_bus_socket
[Install]
WantedBy=sockets.target
This creates the socket at /run/dbus/qm_bus_socket
.
Make sure the systemd socket is enabled:
$ systemctl enable qm-dbus.socket
- Run the DBus daemon with a Quadlet file at
/etc/containers/systemd/qm-dbus-broker.container
:
[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, but similar steps can be used for other compositors.
Firstly, 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:
[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:
[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:
[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:
Enjoy!