A reckless guide to OpenBSD

Part 8 - Remote X11 and sndio

=> This is one of a series of articles - check out the index

Introduction

This week, Jay shows us how to run graphical programs over the network, and forward audio from programs running remotely back to our local workstation.

If you've never used X in a networked configuration before, get ready for an exciting ride!

Why bother?

Running a graphical program remotely, and sending the raw framebuffer data back over the network for display on the local workstation, might sound extremely awkward and inefficient.

With modern high-resolution displays, that's a lot of data to be shuffling around, even between two directly connected machines on a fast LAN.

However, there certainly are advantages to doing this, and numerous interesting aspects that we can make use of.

Let's be clear here that we are not talking about simple `screen and keyboard mirroring' of a remote machine for maintenance purposes. Rather, we're talking about running a program such as a webbrowser on a remote CPU, and interacting with it within our local graphical X session together with other graphical programs running on our machine, as if the program running on the remote CPU was in fact just another X application running normally on the local workstation.

Possibilities of using remote X

Already, some of the potential advantages should have become clear...

This whole concept of a networked windowing system might seem alien to anybody who has only worked in the IT industry in the modern era. CPU and graphics hardware has become cheap enough that even the most basic workstation won't struggle with typical business applications such as web browsers, graphics manipulation programs, and even video editors.

However, the X window system was conceived in a different era, when powerful CPUs and graphics hardware were much more expensive than they are today. It made a lot of sense to allow users to send graphical output elsewhere. This was also relatively straightforward to implement, in part because it was expected to be run across a trusted network, with little need for security.

Whilst the original motivations for remote display might not be as useful in modern times, we can certainly find other ways to put this functionality to work.

Fundamentals

The graphical output that we see from X applications, is produced by an X server. Usually, users interact directly with an X server running on their own workstation. This X server accepts, processes, and forwards keystrokes and pointer movements to the applications, which are it's clients.

Users can also interact with X applications in other ways. They can be started from a text console, accept input from that console's standard input, be controlled by the shell running on that console, and receive signals, just like any regular non-X application.

Communication between the X applications and the X server that they are connected to, takes place over one of several possible communications channels. Typically, connections to an X server running on the same machine will be made using a local UNIX-domain socket, and remote connections will use a TCP socket. A running X server can listen on both a UNIX-domain socket and a TCP socket, and there can be multiple independent X servers running on the same machine.

In either case, the byte stream flowing over the socket connection is the X Window System Protocol. This low-level protocol is an open specification, freely available on the internet.

To set up a networked X11 connection, we essentially only need to configure the X server to listen on a remotely accessible TCP port, configure the client programs to use that same port for their communication with the X server, and ensure that appropriate authentication records are in place to grant access to the X server to the client.

However, in practice, we will also usually want to take steps to encrypt the network traffic.

Important note

Other guides to setting up networked X connections often rely on specific X11 forwarding functionality built into the ssh program. This works slightly differently to the way described here, which just uses the native networking functionality of the X server.

Although the X11 forwarding functionality in ssh can be useful as a quick and easy way of making occasional ad-hoc connections to remote X servers, it comes with a protocol overhead which is not insignificant. Especially for connections over a trusted LAN where encryption is not required, performance will likely be much improved by configuring the connection directly without using an ssh tunnel. Furthermore, understanding how X networking is configured natively may be useful when diagnosing problems running the X protocol over ssh.

In essence, using ssh for X11 forwarding is the lazy solution. If you still want to use the ssh forwarding method instead of the method described here, the manual page for ssh(1) explains it.

Example hostnames

In the examples that follow, we'll be considering two machines. The local workstation, which will run the X server, has hostname desk.lan. The remote machine, which will run the X application and send it's output over the network to the X server running on desk.lan, has hostname rack.lan.

We'll also assume that the local X server is being invoked manually, rather than via a display manager such as xenodm, although most of the same principles apply in both cases.

Selecting one X server from many

Virtually all X applications check the value of the DISPLAY environment variable to find out where to send their graphical output.

When we run a single X server on the local machine, we don't usually need to concern ourselves with the setting of the DISPLAY environment variable. If X is invoked by a user from the command line, then this variable is usually set by xinit.

When accessing remote servers, (or our local server from a remote machine), we need to set it manually.

The first, (and often only), X display on the local machine is designated :0. If the X server is listening on TCP sockets, this will usually be the display that is using port 6000. The same display accessed remotely would be designated by prefixing the display number with the hostname, as hostname:0.

Connect to a remote machine, and point the DISPLAY environment variable back to the local host:

 desk$ ssh rack.lan
 rack$ export DISPLAY=desk.lan:0

Setting the DISPLAY environment variable manually can also be useful if we want to start an X application on the local display, from a text-based virtual terminal also on the local machine. In this case a simple export DISPLAY=:0 will suffice.

Enabling TCP connectivity to the X server

The most commonly used TCP port for the X Window System Protocol is 6000, with any second and subsequent servers using ports 6001 onwards. Although it's possible to run the protocol on an alternative set of ports, most software sets this port range at compile time, so doing so is likely to be cumbersome.

The command line option to enable listening on a particular connection type is -listen:

Start the X server, configured to allow inbound TCP connections:

 desk$ X -listen tcp

Of course, starting a bare X server with no window manager is not usually what we want to do. It might be useful if we want to build a dedicated X terminal, but in most cases we will want to start our regular X environment instead:

Run the startx script, but allow inbound TCP connections:

 desk$ startx -- -listen tcp

Note the use of -- to signal the end of the argument list to the startx script itself, to allow the -listen option to be passed directly to the X server.

The -listen tcp option will configure the X server to listen on both IPv4 and IPv6 addresses. To restrict listening to a single protocol, we can use the options -listen inet6 for IPv6, or -listen inet for legacy IPv4.

By default, the X server listens on all interfaces. The firewall ruleset that comes in the base OpenBSD installation blocks TCP ports 6000 to 6010 on all interfaces except localhost, so we need to adjust this as necessary to grant access to the hosts that will be connecting.

Once we've enabled listening of the X server via a TCP port, then assuming that network connectivity between the machines is correctly configured, with no firewall blocking access, we can connect to the X server from the remote machine:

Test network-level connectivity:

 rack$ nc -v desk.lan 6000
 Connection to desk.lan (2001:db8::1) 6000 port [tcp/*] succeeded!

Once we have basic network connectivity to the correct port, we can move on to configuring X authentication records.

Configuring X authentication records

If we try to run a program on the remote host and direct it's graphical output back to our local server, we'll see that it doesn't work:

 desk$ ssh rack.lan
 rack$ export DISPLAY=desk.lan:0
 rack$ xclock
 No protocol specified
 Error: Can't open display: desk:0

Connecting to an X server without first configuring authentication records isn't much fun!

But at least TCP/IP connectivity is working.

Note that the above error, specifically the `No protocol specified', implies that connectivity at the TCP/IP networking level is working.

If we were, instead, trying to connect to a closed port, such as might occur if the X server wasn't actually running on desk.lan, then we would just see the `can't open display' line:

 desk$ ssh rack.lan
 rack$ export DISPLAY=desk.lan:0
 rack$ xclock
 Error: Can't open display: desk:0
Connecting to a closed port which immediately returns a RST packet results in the absense of the 'No protocol specified' line, and suggests a network connectivity issue.

No protocol specified? Huh???

The `no protocol specified' error message may appear somewhat cryptic to users who are not familar with networked X connections. The error message effectively means that no credentials have been supplied to authenticate with the remote machine. The protocol it refers to is an authorisation protocol. The design of the X Window System Protocol allows for various different authorisation protocols to be used, and since we haven't provided any credentials at all, obviously no protocol has been specified either, hence the reference to this in the error message.

The authentication protocol that we will be using is a simple 128-bit cookie that is generated by the X server, and passed to the remote host to which we want to allow access. The remote host then passes the cookie back whenever it needs to authenticate itself. The name of this protocol is MIT-MAGIC-COOKIE-1.

MIT-MAGIC-COOKIE-1 is your friend <3

Important note

Although the X window system supports various `security features', these are extremely primative by today's standards.

For effective access control, privacy, and confidentiality, the transport of the X Window System Protocol across the network should be protected by an encrypted tunnel. Access to the open network ports on the host should additionally be restricted with the use of a firewall, to further protect against the potential exploit of any as yet un-discovered vulnerabilities that might be present in the X server.

The native security features are still somewhat useful, however, to protect against mis-configuration or casual operator errors such as connecting to the wrong X server on a network. For this reason, we configure them rather than simply globally disable them.

To generate a suitable cookie, we use the xauth program, and we can easily transfer it to the remote machine using scp:

 desk$ xauth -f new_cookie generate desk.lan:0 .
 xauth:  file new_cookie does not exist
 desk$ scp -p new_cookie desk:.Xauthority

The `file new_cookie does not exist' message is informational, and not an error. If the file does already exist, it will be silently overwritten.

Note the use of the -p argument to scp. The file created by xauth has it's permissions set to 0600, and we want to ensure that the transferred file is only accessible by the user, and not group or world readable.

At this point, we can run X applications on rack.lan, and their output will appear on the X server running on desk.lan:

 desk$ ssh rack.lan
 rack$ export DISPLAY=desk.lan:0
 rack$ xclock

Important note

Access to an X server can also be controlled via a host-based access control list. This is usually manipulated by the xhost program.

By default on OpenBSD, host-based access control is enabled, and the list of permitted hosts is empty. In this configuration, no connections are permitted, (including local connections via UNIX-domain sockets), unless authorised via an authentication record created by xauth.

Although the xhost program and it's functionality still exist, in most cases there is little reason to use this method of access control instead of the user-based cookie system described above. Additionally, xhost doesn't support the concept of trusted and untrusted connections that xauth does, so all connections authenticated in this way will effectively be trusted connections, as explained in the next section.

Trusted verses untrusted connections

By default, authorisations created using `xauth generate' are untrusted. The concept of trusted and untrusted clients provides us with a way to implement a very limited form of isolation between different client programs.

Many users don't realise the extent to which different client applications using the same X server are able to interact with each other via the window system itself.

An obvious example would be taking a screenshot of the whole desktop:

 desk$ xwd -root -out screenshot.xwd

However, an X application can also send keystrokes to another X application, and change it's properties. For example, we can start xclock, and from a completely separate instance of xterm, change the title displayed by the window manager for the xclock window:

 desk$ xprop -name xclock -set WM_NAME chronograph

Ignoring the fact that this might be undesirable even in a local single user environment, it's quite easy to see that it's even less of a good idea when the X server accepts connections from other hosts.

If we try the same command from rack.lan, however, by default it fails:

 desk$ xclock 
 desk$ ssh rack.lan
 rack$ export DISPLAY=desk.lan:0
 rack$ xprop -name xclock -set WM_NAME chronograph
 X Error of failed request:  BadAccess (attempt to access private resource denied)
   Major opcode of failed request:  18 (X_ChangeProperty)
   Serial number of failed request:  27
   Current serial number in output stream:  29
Trying but failing to modify the WM_NAME property of a locally run program from an untrusted remote session offers a small degree of protection. Unfortunately, that's about as far as this security feature goes.

This happens because the authorisation that we created for our user on rack.lan is untrusted. Untrusted clients are limited in what they can do to trusted clients. And unfortunately, that's about as far as this security feature goes. There is a broad two-way distinction between two classes of client, but there is nothing to stop one untrusted client from interfering with another untrusted client, nor to stop a trusted client from interfering with anything.

Handy hint!

Untrusted clients will crash if they try to access the root window, or the terminal bell.

This latter restriction can be a source of much frustration when editing a text file in vi, and hitting escape once too often. Invoking xterm with the visual bell option -vb is a convenient workaround for this.

Copy and paste is also not supported from untrusted to trusted clients, although text copied from trusted clients can be pasted into untrusted ones. Certain other operations are also restricted.

Despite these limitations, however, overall most X client programs will work quite happily when run in an untrusted environment.

We could, of course, have just created a trusted authorisation all along:

 desk$ xauth -f new_cookie generate desk.lan:0 . trusted

Now, clients connecting and authenticating using this cookie will not have the restrictions we described above placed on them. The example above of renaming the xclock window would succeed.

Once again, this security feature is very limited in it's scope. Just because a client is `trusted' doesn't imply anything beyond it having unlimited access to the other resources on the X server.

Format of the .Xauthority file

The format of the .Xauthority file is defined in /usr/xenocara/lib/libXau/include/X11/Xauth.h, as the `xauth' structure. The .Xauthority file we generated earlier on desk.lan would look something like this:

00000000  00 06 00 10 20 01 0d b8  00 00 00 00 00 00 00 00  |.... ...........|
00000010  00 00 00 01 00 01 30 00  12 4d 49 54 2d 4d 41 47  |......0..MIT-MAG|
00000020  49 43 2d 43 4f 4f 4b 49  45 2d 31 00 10 45 78 4f  |IC-COOKIE-1...?Q|
00000030  74 49 63 53 69 4c 69 43  6f 4e a6 af bc           |D..gG~<......|
00000000  00 06 00 10 20 01 0d b8  00 00 00 00 00 00 00 00  |.... ...........|
00000010  00 00 00 01 00 01 30 00  12 4d 49 54 2d 4d 41 47  |......0..MIT-MAG|
00000020  49 43 2d 43 4f 4f 4b 49  45 2d 31 00 10 45 78 4f  |IC-COOKIE-1...?Q|
00000030  74 49 63 53 69 4c 69 43  6f 4e a6 af bc           |D..gG~<......|
00000000  00 06 00 10 20 01 0d b8  00 00 00 00 00 00 00 00  |.... ...........|
00000010  00 00 00 01 00 01 30 00  12 4d 49 54 2d 4d 41 47  |......0..MIT-MAG|
00000020  49 43 2d 43 4f 4f 4b 49  45 2d 31 00 10 45 78 4f  |IC-COOKIE-1...?Q|
00000030  74 49 63 53 69 4c 69 43  6f 4e a6 af bc           |D..gG~<......|
00000000  00 06 00 10 20 01 0d b8  00 00 00 00 00 00 00 00  |.... ...........|
00000010  00 00 00 01 00 01 30 00  12 4d 49 54 2d 4d 41 47  |......0..MIT-MAG|
00000020  49 43 2d 43 4f 4f 4b 49  45 2d 31 00 10 45 78 4f  |IC-COOKIE-1...?Q|
00000030  74 49 63 53 69 4c 69 43  6f 4e a6 af bc           |D..gG~<......|
00000000  00 06 00 10 20 01 0d b8  00 00 00 00 00 00 00 00  |.... ...........|
00000010  00 00 00 01 00 01 30 00  12 4d 49 54 2d 4d 41 47  |......0..MIT-MAG|
00000020  49 43 2d 43 4f 4f 4b 49  45 2d 31 00 10 45 78 4f  |IC-COOKIE-1...?Q|
00000030  74 49 63 53 69 4c 69 43  6f 4e a6 af bc           |D..gG~<......|

Note: The IP address is included in the .Xauthority file...

As we can see, the IP address of the host - 2001:db8::1 - is included in the structure along with the actual 128 bits of cookie data.

This detail might seem unimportant now, but will later prove to be useful.

Fun fact!

If you're wondering where the X server stores it's copy of the authentication credentials, it's in the file specified by the -auth argument when invoking X. On OpenBSD, this will usually be a temporary file with a name in the format of $HOME/.serverauth.XXXXXXXXXX, where XXXXXXXXXX are random characters.

Timeouts

Authentication records created with xauth expire and are removed from the X server if they remain idle, with no connections using them, for a set period of time.

Trying to connect to the X server using the cookie we generated above after it has expired will return an 'Invalid MIT-MAGIC-COOKIE-1 key' error message:

 Invalid MIT-MAGIC-COOKIE-1 key

Often, although not always, caused by using an expired cookie.

By default, the timeout period is set to 60 seconds. However, this can be increased by supplying the timeout argument to xauth when generating the new cookie:

 desk$ xauth -f new_cookie generate desk.lan:0 . timeout 120

Multi-homed hosts

If your local workstation is multi-homed, some extra considerations might be necessary if you want remote hosts to be able to connect to an X server running on an IP address that is not associated with your machine's hostname.

Think ahead!

Although the X server might not be running on a physically multi-homed machine, if we set up an IPSEC tunnel as described in the next section to encrypt the network traffic, the local machine will gain an extra IP address for the tunnel endpoint, thus making it multi-homed.

We'll assume that our host desk.lan is at IPv6 address 2001:db8::1, but also has IPv6 address 2001:db8:ffff::1, which is listed in DNS as workstation.lan.

In this case, the local .Xauthority file will contain two entries. The first will be of address family FamilyLocal, containing the literal hostname desk.lan' as the address. The second will be of address type FamilyInternet6, (which is defined in /usr/xenocara/proto/xorgproto/include/X11/X.h), containing the literal IPv6 address of that hostname, 20010db8000000000000000000000001'. Even though desk.lan is listening on port 6000 of 2001:db8:ffff::1, no entry will be created in our local .Xauthority file for this IP address.

If we want to connect from rack.lan to workstation.lan, we need to generate a cookie containing the correct IP address of 2001:db8:ffff::1, (remember the important detail mentioned above). Since xauth needs to connect to the server to run the generate command, and we don't have an entry in the local .Xauthority file to connect to that address, we can't generate the necessary cookie to send to rack.lan.

There are various ways to overcome this. One way is to edit /usr/X11R6/bin/startx and replace the code that sets the environment variable hostname from the output of /bin/hostname with a hard-coded assign to `workstation.lan'. Alternatively, we could create our own 128-bit key as a series of 32 hex digits, and add it to both the server's authority file and the .Xauthority file on rack.lan:

 Manual keying
 desk$ xauth -f .serverauth.oYUN9tQusv add desk.lan:0 . a1850df6d42eb2f1b9ab332e15915b31
 desk$ xauth -f new_cookie add desk.lan:0 . a1850df6d42eb2f1b9ab332e15915b31
 desk$ scp -p new_cookie desk:.Xauthority
 desk$ ssh rack.lan
 rack$ export DISPLAY=desk.lan:0
 rack$ xclock

Adding a specific 128-bit cookie to the server authorisation file, and also sending it to the remote machine

Note however, that unlike authorisation entries added using `generate', those added in this way can't be set as untrusted.

Running the X Window System Protocol through an IPSEC tunnel

Since the native X Window System Protocol is unencrypted, if we want to run it over anything other than a private LAN with other users that we trust, some sort of encrypted tunnelling is obviously desirable.

One way to do this is by using IPSEC. If you've read the previous installment of this series, you'll know how to generate the necessary keys and certificates for iked. Once this is done, to tunnel port 6000 between our two machines desk.lan and rack.lan, we first configure a new virtual interface on each:

 desk# ifconfig vether0 create
 desk# ifconfig vether0 inet6 2001:db8:ffff::1
 desk# echo "inet6 2001:db8:ffff::1" > /etc/hostname.vether0
 rack# ifconfig vether0 create
 rack# ifconfig vether0 inet6 2001:db8:ffff::2
 rack# echo "inet6 2001:db8:ffff::2" > /etc/hostname.vether0

Hostnames for these new IP addresses obviously either need to be added to DNS or the /etc/hosts file on each peer:

 # echo "2001:db8:ffff::1    workstation.lan" >> /etc/hosts
 # echo "2001:db8:ffff::2    erack.lan" >> /etc/hosts

Now we just need to configure iked:

On the local workstation running the X server:

 ikev2 active esp proto tcp from workstation.lan port 6000 to erack.lan peer rack.lan ecdsa384

On the remote host running the X application:

 ikev2 esp proto tcp from erack.lan to workstation.lan port 6000 peer desk.lan ecdsa384

At this point we should be able to run X applications on rack.lan, and have their output sent to the local X server over the encrypted tunnel:

 desk$ xauth -f new_cookie generate workstation.lan:0 .
 desk$ scp -p new_cookie desk:.Xauthority
 desk$ ssh rack.lan
 rack$ export DISPLAY=workstation.lan:0
 rack$ xclock

A primer on sndiod

Whilst our graphical programs running on a remote machine are now happily displaying their output on our workstation's local X server, our speakers are eerily quiet.

Luckily, OpenBSD comes with a network capable audio abstraction daemon in the base installation, in the form of sndiod. We can use this to establish remote access to our audio devices, in much the same way as we've just set up remote access to our X server.

In a default installation of OpenBSD, sndiod is already set to run in /etc/rc.conf, but only runs locally and doesn't accept or make any network connections, listening only on a local UNIX-domain socket.

The default TCP port assignment for sndiod is 11025 for the first unit configured, and subsequent ports for the following units. In practice, many setups will only have a single unit configured, using port 11025. Again, this is broadly similar to the way that the X Window System Protocol uses a range of ports starting at 6000 for the first configured display.

Just like the X Window System Protocol, the bytestream that sndiod will send over the network is unencrypted. If this is an issue, we can solve it in the same way as we did before, by creating an IPSEC tunnel for the connections.

The sndiod protocol doesn't include any kind of access control at the protocol level, except for being able to specify the bind address on the server, and using a 128-bit session cookie. The session cookie mostly serves to prevent two different users from connecting to the same sndiod server simultaneously. As a result, controlling access at the network level with suitable firewall rules is essential. Failing to do this will essentially give all remote users the ability to connect to our sndiod server. This in turn, depending on which audio devices are being shared, could give them access to our local speakers, microphone and other audio inputs.

Additionally, any user on the remote machine who is able to access the sndiod cookie, (for example, the root user), will be able to join in the current session, so appropriate care should be taken if you don't have exclusive root access on the remote machine.

Unfortunately, in stark contrast to the X Window System Protocol, the actual format of the sndiod bytestream doesn't seem to be documented anywhere. The source for /usr/src/usr.bin/sndiod/sock.c might provide some clues if you have the patience to read through almost 2000 lines of poorly commented code, littered with single character variable names.

Setting up sndiod for remote audio

Configuring sndiod to listen for incoming connections on a TCP port is done using the -L argument:

 desk# /etc/rc.d/sndiod stop
 sndiod(ok)
 desk# sndiod -dd -L desk.lan

Here we are running sndiod in debug mode, so that we can see inbound connections for troubleshooting purposes. For production use, we would usually want to add a line to /etc/rc.conf.local:

 desk# echo "sndiod_flags=-L desk.lan" >> /etc/rc.conf.local

This is the most basic configuration, in which we export access to both the recording and playback devices.

Connecting to a remote sndiod instance

Setting the environment variable AUDIODEVICE allows us to control where audio input and output is sent.

The following commands will set the default audio device and play a 1 Khz tone:

 rack$ export AUDIODEVICE=snd@desk.lan/0
 rack$ perl -e 'print ((((chr(0).chr(8)) x 48).((chr(0).chr(248)) x 48))x1000);' | aucat -i -

An IPSEC tunnel for sndio traffic

Adding port 11025 to the ipsec.conf that we saw earlier is straightforward:

On the local workstation running the X server:

 ikev2 active esp proto tcp from workstation.lan port 6000 to erack.lan from workstation.lan port 11025 to erack.lan peer rack.lan ecdsa384

On the remote host running the X application:

 ikev2 esp proto tcp from erack.lan to workstation.lan port 6000 from erack.lan to workstation.lan port 11025 peer desk.lan ecdsa384

We can then run the sndiod server set to listen on hostname workstation.lan instead of desk.lan:

Set sndiod to listen on the correct address:

 sndiod_flags=-L workstation.lan

The only change necessary on the remote host is to the AUDIODEVICE environment variable:

 rack$ export AUDIODEVICE=snd@workstation.lan/0

Summary

This week we saw how to configure a local X server to accept connections from remote clients over the network, looked at some legacy security features of the X window system, explored the actual structure of the .Xauthority file, and then looked at how to secure the transport of the data stream using an encrypted IPSEC tunnel. As well as all that, we set up sndiod to listen over TCP so that we could send audio data from the same remote hosts that are connecting to the X server!

Don't miss next week's installment, where we'll be covering upgrading by re-installing.

In next week's installment, Jay will be showing us how to re-install openbsd quickly and easily. Upgrading, or downgrading along the way will be a piece of cake! Don't be late!

=> Continue to part nine

=> Home page of the Exotic Silicon gemini capsule. | Your use of this gemini capsule is subject to the terms and conditions of use.

Copyright 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Exotic Silicon. All rights reserved.

Proxy Information
Original URL
gemini://gemini.exoticsilicon.com/series/reckless_guide_to_openbsd/remote_X_and_sndio
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
437.19806 milliseconds
Gemini-to-HTML Time
4.198898 milliseconds

This content has been proxied by September (3851b).