1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
|
======================
Docker X11 Passthrough
======================
Briefcase can use Docker to build apps for Linux distributions other than the
distribution you're currently using. Docker can also be used to *run* the app
on the foreign distribution, exporting the graphical aspects of the app to your
local display. This document describes how to configure your system to do this.
X Window System Background
--------------------------
Linux distributions use either the `X Window System <https://www.x.org/>`_
(sometimes called X or X11) or `Wayland <https://wayland.freedesktop.org/>`__
to manage their graphical displays. X11 is the older of the two; Wayland
maintains compatibility with the `X11 protocol
<https://www.x.org/releases/X11R7.7/doc/xproto/x11protocol.html>`__ for
backwards compatibility.
The X11 protocol operates in a client/server framework; any application that
wishes to display a window or receive user input will send and receive commands
with an X server.
X Configuration
~~~~~~~~~~~~~~~
The location of the X server is declared by the ``DISPLAY`` environment
variable, normally in the form ``HOSTNAME:DISPLAYNUMBER.SCREENNUMBER``. If
``HOSTNAME`` is absent, it is assumed to be the machine the client is running
on. The ``SCREENNUMBER`` is largely historical since all monitors are normally
collapsed together in to a single screen now. Therefore, an expected setting
for ``DISPLAY`` is ``:0`` for many installations.
While security is relatively weak for X11, there are basic facilities to
mitigate unauthenticated access. The ``XAUTHORITY`` environment variable can
specify a file path to an ``xauth`` database; if ``XAUTHORITY`` is not set,
there will normally be a system default file path configured for each user. The
``xauth`` database itself is protected by file system access controls and
will contain "cookies" for individual displays that are assigned by the X
server for clients to use to facilitate authentication.
An ``xauth`` database authorizing a host named ``jupiter`` might look like:
.. code-block:: console
$ xauth list
jupiter/unix: MIT-MAGIC-COOKIE-1 9e9a67185b1fdc0c46e00dc400559873
#ffff#6a757069746572#: MIT-MAGIC-COOKIE-1 9e9a67185b1fdc0c46e00dc400559873
Along with cookie-based authentication, it is also possible to add entities to
an allowlist for a display. For instance, some distributions are configured to
allow any process owned by the logged in user access to the display. This is
configured by ``xhost``.
An ``xhost`` configuration authorizing a user named ``brutus`` might look like:
.. code-block:: console
$ xhost
access control enabled, only authorized clients can connect
SI:localuser:brutus
X Operation
~~~~~~~~~~~
While authentication is normally enabled for X access, security is mostly
bolstered by the allowed methods for clients to connect. Most systems will only
open a UNIX socket to which clients should connect to send and receive
messages. By virtue of file system design, only users on the host machine will
have access to this socket. With the introduction of abstract sockets in Linux,
such a socket is also typically made available in tandem with the UNIX socket.
The advantages of an abstract socket is beyond this discussion, though.
Such a UNIX socket connection will be configured for each display for the
machine. As with many things in Linux, UNIX sockets are exposed in the machine
via a file in the ``/tmp/.X11-unix/`` directory. The socket files are named for
the number of the DISPLAY they are connected to; so, the file for Display 0 is
``X0``. Therefore, any display can be found at ``/tmp/.X11-unix/X#``.
Along with a UNIX socket connection, X servers can also listen on a TCP socket
on the machine's network interfaces. However, since a network connection can
easily be reached by other machines, listening on a TCP socket is normally
disabled on most Linux distributions. That said, the X11 standard reserves port
numbers starting at port 6000 for X displays. Therefore, Display 0 is available
at port 6000 while Display 99 would be available at port 6099 and so on.
Docker
------
From the design of X11, it is clear that a Docker container needs access to the
socket for the display and the ``xauth`` database for the user.
An example of a well-published method to accomplish this:
.. code-block:: console
$ docker run --rm -it --net host --env DISPLAY ubuntu xeyes
This method assumes:
- the user in the container is ``root`` or its ID matches the host user
- ``xhost`` is configured to allow any connections from the user
- the X server is running an abstract socket for the display
By virtue of how access control is managed for abstract sockets, the
``--net host`` configuration allows access to the abstract socket for the
display inside the container and passing the ``DISPLAY`` environment variable
through lets the ``xeyes`` application know which display to connect to.
While this strategy works in many environments, the generalized solution is
more complicated to accommodate variations in Docker implementations as well as
whether the current display is actually being proxied.
Docker Desktop
~~~~~~~~~~~~~~
The original implementation of Docker is referred to as Docker Engine and
leverages many advanced features of Linux to run processes in highly
containerized environments. Docker Desktop, however, effectively runs Docker
Engine inside a lightweight Linux virtual machine (VM) running on the host
machine. Therefore, when Docker Desktop runs a container, it is running it
inside of a VM and not directly on the host system as Docker Engine does
(albeit in isolation via containerization).
As an outcome of the design of Docker Desktop, the behavior of interactions
between the host machine and Docker containers can be significantly different.
For instance, it is not possible to expose the host machine's network to a
container via ``--net host`` like you can with Docker Engine. While this does
change the exact network configuration that's exposed to the container in the
Docker Desktop VM, it is much different than Docker Engine and abstract sockets
on the host are not available to the container.
Along with not being possible to expose abstract sockets on the host to a
container running via Docker Desktop, it is also not possible to expose
arbitrary UNIX sockets either. Therefore, attempting to bind mount
``/tmp/.X11-unix/X0``, for instance, in to a Docker Desktop container will not
allow processes inside the container to successfully communicate with the
socket. (The Docker team has added support to pass specific sockets such as the
socket Docker itself uses, as well as the SSH agent socket; but exposing
arbitrary sockets has been deemed out of scope for now.)
Therefore, since it is not possible to expose a socket for an X display to a
container running in Docker Desktop, the X display will need to be exposed over
the network shared by the host and container.
Docker Networking
~~~~~~~~~~~~~~~~~
In Docker Engine, networking is relatively straightforward. On the host, a
network interface bridge called ``docker0`` is installed. This bridge serves
to mediate communication among containers as well as between containers and the
host. If the host would like to expose a network-based service to a container,
it can bind to a port on ``docker0`` and containers can connect to it.
In Docker Desktop, however, the Linux VM in which containers run complicates
matters. Inside the Linux VM, it's largely a similar configuration with a
network bridge but the host machine cannot directly interact with this bridge
interface. Instead, the host's network interface is assigned an address on the
bridge similar to how other containers are. In this way, containers can still
connect to network-based services on the host but not through a shared network
interface called ``docker0``.
To help simplify the configurations for applications running inside Docker
Desktop containers, the hostname ``host.docker.internal`` will always resolve
to an IP address for the host's network interface and thereby allow access to
network-based services on the host.
Unlike Docker Desktop, Docker Engine cannot intercept DNS requests from
containers; therefore, ``host.docker.internal`` must be configured when the
container is started. This is accomplished via the ``--add-host`` option which
allows mapping a hostname to an arbitrary address for the hostname. This
mapping is applied by writing it in the container's ``/etc/hosts`` file. Using
``--add-host``, ``host.docker.internal`` is mapped to the keyword
``host-gateway``. This keyword is a special value that the Docker server will
replace with an address from which the host will be reachable within a
container whether it is Docker Engine or Docker Desktop starting it.
In conclusion, we can add ``--add-host host.docker.internal:host-gateway`` to
the options to start a container and the host network interface will be
reachable at ``host.docker.internal``.
Exposing an X Display to a Container
------------------------------------
Given the knowledge of the operation of the Docker implementations, we finally
have the pieces to expose an X display to a container. Since it is not possible
to expose the display's socket directly to a container, a TCP proxy is
configured to pass X messages on the network from the container to the socket
on the host machine for the display.
TCP Proxy
~~~~~~~~~
The `socat <https://repo.or.cz/socat.git>`__ tool is a widely available program
to relay bi-directional data transfer between independent data channels. It
allows running a process on the host to listen on a network port and send any
received data to a socket connected to an X display on the other side.
Creating a TCP proxy for the X display effectively creates a spoofed X display.
The proxy is configured to listen on the TCP port for an unallocated display;
the port number will be 6000 + the number of the display. Additionally, the
proxy is configured to listen on all network interfaces since identifying the
exact interface that will be available within the container is non-trivial.
The other side of the proxy is connected to the socket for the X display. The
socket, though, for the display may actually be another TCP socket; this will
be the case if the environment is currently configured for X11 forwarding over
SSH, as discussed below. In most cases, though, the socket will be the UNIX
socket for the display in the ``/tmp/.X11-unix/`` directory.
X Authentication for the Proxied X Display
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Authentication for X displays is managed in ``xauth`` database files. The
``xauth`` program allows for reading and writing the database. The database is
relatively simple mapping of display information to a cookie. When a client
wants to establish a connection for a display, it queries the database for the
display and receives the cookie back.
Since the proxy creates a spoofed display, a new ``xauth`` database needs to be
created for the spoofed display using the authentication afforded to the user
for the current display.
To create a new database, you need to:
- Extract the cookie for the current display
- Create a new database file
- Add an entry for the spoofed display using the extracted cookie to the new
database
- Rewrite the hostname of the entry that was just created to be "FamilyWild" (a name
`reserved by the Xauth specification to match all displays
<https://www.x.org/archive/current/doc/man/man7/Xsecurity.7.xhtml>`__)
This new ``xauth`` database file is set in the ``XAUTHORITY`` environment
variable for the container so any X connections use it.
The hostname must be updated in the new database file because when the new
entry is created, the ``xauth`` program will associate the host machine's
hostname with the display. In the container, though, the ``DISPLAY`` variable
will be using ``host.docker.internal`` as the hostname for the display. If it
is not updated, then the authentication cannot be used. Furthermore, the
``xauth`` program will not allow creating authentication entries for displays
that do not actually exist. So, we manually update the hostname of the entry to
a wildcard value such that queries for the display number will return the
authentication regardless of the hostname of the query.
X11 Forwarding over SSH
-----------------------
A common practice is to forward X11 communication from a remote machine to the
local machine when using SSH. Therefore, when someone establishes an SSH
connection to another machine and runs Briefcase, this X11 passthrough
mechanism should passthrough the X11 forwarding for SSH in to the Docker
container.
When X11 forwarding is configured for SSH, there are multiple channels
established between the local and remote machine. The primary channel
facilitates the interactive shell session; additionally, though, SSH sets up
another channel for the X communication.
It accomplishes X11 forwarding in much the same way that Briefcase is proxying
X communication from the Docker container to the host. On the remote machine,
the X11 channel is bound to the TCP port for a spoofed display and creates a
new entry in the user's ``xauth`` database for the display. Unlike Briefcase's
proxy, the SSH proxy actively modifies some of the X messages; it will verify
connection attempts use the authentication created in the database by SSH and
will replace it with the actual authentication used on the local machine.
Since Briefcase will first connect to a TCP socket for a display, it will find
the spoofed display created by SSH and create the proxy such that it connects
to that TCP socket. In this way, the container sends X messages to the proxy,
the proxy send them to the SSH X11 channel, and SSH translates them for the X
display on the local machine.
|