Diskless Linux X Terminals

by Chip Coldwell

The X terminal is not a new idea; companies such as NCD have been manufacturing them for 15 years or more. The thin client idea fell out of fashion during the late 1990s, however, as the price of PC hardware fell so low that there was no obvious cost advantage to using X terminals. Heated arguments ensued over the total cost of ownership (including both the cost of the hardware and administrative support) of thin clients vs. PCs, and the debate will not be resolved by this article. The objective here is simply to describe a technique that allows one to utilize some of the growing pile of obsolete hardware left in the wake of advancing PC technology to build X terminals.

The essential characteristic of any thin client is that it should have little or no persistent storage. Typically, a purpose-built X terminal has a small quantity of NVRAM used to store configuration options and nothing else. In practice, it usually is possible to put even these options in a configuration file stored on the server and downloaded by the terminal on boot. This article takes the purist view that an X terminal should have no persistent storage whatsoever.

PXE Booting

The PC has no hard, floppy or CD-ROM drive, so some other device must provide the bootloader and bootable image. X terminals are creatures of the network they inhabit, so the obvious choice is the network interface card (NIC). The NIC, therefore, must identify itself to the BIOS as a bootable device. If chosen, it must be able to download the bootloader from the network. This is not something most run-of-the-mill NICs can do. However, a standard for NIC boot ROMs called PXE (Preboot eXecution Environment, pronounced pixie) has been published by Intel and implemented by that company as well as by some other vendors in some products. Many newer motherboards with built-in Ethernet have PXE support.

In preparing this article, I tested five different kinds of NICs, all of which were advertised to support PXE: the Intel PRO/100+ (PILA8460BNG1), the 3Com 3C905CX-TX-M, the D-Link DFE-550TX, the Linksys LNE100TX and the SMC 1255TX (Tulip chipset). Of these five, only the 3Com card worked right out of the box. I was able to get a boot ROM separately for the SMC card, after which it also worked. The other three cards all had conspicuous but vacant sockets for boot ROMs, which were not shipped by default. Caveat emptor.

When the PXE NIC is chosen by the motherboard BIOS as the boot device, it broadcasts DHCP requests on the LAN and looks for PXE extensions in the responses it receives. If it receives a response containing some of these extensions, it then acknowledges and accepts the response. In particular, it respects the next-server and filename parameters in the server's response. These parameters specify the IP address of a TFTP server and the name of the file containing a bootloader that the client should download and start.

DHCP and TFTP

The Internet Software Consortium's version 3.0 DHCP server can be configured to advertise PXE extensions, and it is the DHCP server shipped with a number of Linux distributions, including Red Hat 8.0 and later versions. Listing 1 is an example of a DHCP server configuration file, dhcpd.conf, that generates DHCP responses with PXE extensions when the DHCP client identifies itself as a PXE NIC. With this configuration, the client downloads the file pxelinux.0 from the TFTP server, located at 192.168.1.1. Table 1 lists the options set in the configuration file.

Table 1. Definitions of PXE-Specific Codes in dhcpd.conf

CodeMeaning
1Multicast IP address of boot file server.
2UDP port that client should monitor for MTFTP responses.
3UDP port that MTFTP servers are using to listen for MTFTP requests.
4Number of seconds a client must listen for activity before trying to start a new MTFTP transfer.
5Number of seconds a client must listen before trying to restart an MTFTP transfer.

Listing 1. Example dhcpd.conf File to Support PXE Clients


option space PXE;
option PXE.mtftp-ip
  code 1 = ip-address;
option PXE.mtftp-cport
  code 2 = unsigned integer 16;
option PXE.mtftp-sport
  code 3 = unsigned integer 16;
option PXE.mtftp-tmout
  code 4 = unsigned integer 8;
option PXE.mtftp-delay
  code 5 = unsigned integer 8;
option PXE.discovery-control
  code 6 = unsigned integer 8;
option PXE.discovery-mcast-addr
  code 7 = ip-address;

subnet 192.168.1.0 netmask 255.255.255.0 {

  class "pxeclients" {
    match if substring (option
      vendor-class-identifier, 0, 9) = "PXEClient";
    option vendor-class-identifier "PXEClient";
    vendor-option-space PXE;

    # At least one of the vendor-specific PXE
    # options must be set in order for the client
    # boot ROMs to realize that this is a PXE-
    # compliant server. We set the MCAST IP address
    # to 0.0.0.0 to tell the boot ROM that we can't
    # provide multicast TFTP.

    option PXE.mtftp-ip 0.0.0.0;

    # This is the name of the file the boot ROMs
    # should download.
    filename "pxelinux.0";
    # This is the name of the server they should
    # get it from.
    next-server 192.168.1.1;
  }

  pool {
    max-lease-time 86400;
    default-lease-time 86400;
    range 192.168.1.2 192.168.1.254;

   # If you include this, you must provide host
   # entries for every client, optionally associating
   # ethernet MAC addresses with IP addresses.

   # deny unknown clients;
  }
}

Obviously, the server at 192.168.1.1 must be configured to provide the TFTP service. It also must have a bootloader image called pxelinux.0, where the TFTP server process looks for it (usually in the directory /tftpboot). The TFTP server process usually is managed by one of the superservers, inetd or xinetd, so turning it on means messing around with one of their configuration files (/etc/inetd.conf or /etc/xinetd.conf, respectively).

The file pxelinux.0 is a bootloader that comes from H. Peter Anvin's SYSLINUX Project. Unlike generic bootloaders, such as LILO or GRUB, PXELINUX understands the PXE protocol and has the necessary networking functionality to pick up the boot process at this point by downloading the kernel and compressed RAM disk using TFTP. However, PXELINUX requires an enhanced TFTP server, one that understands the TSIZE option (RFC 2349). Fortunately, H. Peter Anvin also provides a modified version of the standard BSD TFTP dæmon, called tftp-hpa, that does support this option. The easiest thing to do is to replace the standard TFTP dæmon, often located at /usr/sbin/in.tftpd, with tftp-hpa.

PXELINUX

PXELINUX knows where the PXE boot ROMs stashed the network parameters from the DHCP server's response in memory, and it can use these to start another TFTP session to download its configuration file from the server. With the TFTP server configured as described above, the bootloader running on the client first tries to find its configuration file in /tftpboot/pxelinux.cfg/ethermac, where ethermac represents the client's Ethernet hardware address in lowercase hexadecimal, with octets separated by hyphens, for example, fe-ed-de-ad-be-ef. Failing that, the bootloader tries /tftpboot/pxelinux.cfg/iphex, where iphex is the client's IP address in uppercase hexadecimal. For example, if the client has the IP address 192.168.0.12, PXELINUX would try to download the file /tftpboot/pxelinux.cfg/C0A8000C. If that file doesn't exist, the least significant nibble is dropped from the name and the process repeats. Therefore, in the example above, after C0A8000C is not found, PXELINUX tries C0A8000, then C0A800 and so on. This makes it possible to have a single configuration file for an entire subnet, provided that the subnet boundary is nibble-aligned.

Listing 2 shows the contents of a PXELINUX configuration file. The first line gives the name of a file containing a compressed kernel image to be downloaded—all paths are relative to /tftpboot on the server. The second line lists parameters that should be passed to the kernel specifying that the root filesystem be a 64MB RAM disk that should be mounted read/write. The last line causes PXELINUX to generate an additional kernel parameter containing ip=client-ip:server-ip:gateway-ip:netmask and using the values obtained by the PXE boot ROMs from the DHCP server's response. This is useful if the kernel was built with kernel-level autoconfiguration of IP networking enabled. If so, when the kernel boots it uses these parameters to configure the network interface, making it unnecessary to do so using ifconfig or ifup in a startup script.

Listing 2. The PXELINUX configuration file specifies the compressed kernel image to be downloaded.

DEFAULT vmlinuz
APPEND initrd=ramdisk.gz ramdisk=65536 root=/dev/ram rw
IPAPPEND 1
Building the Kernel

In order to use kernel-level autoconfiguration of IP parameters, the network device driver must be available early in the boot, even before the root filesystem is mounted. Therefore, it cannot be a loadable module. Because the kernels that come with most distributions use loadable modules extensively, in practice this means it is necessary to build a custom kernel for the X terminal. In addition, the custom kernel should support RAM disks and initial RAM disks. Kernel-level autoconfiguration of IP networking is also convenient. It is not necessary to include any of the dynamic methods of obtaining IP addresses (DHCP, BOOTP and RARP can be selected), however, as the IPAPPEND line included in the PXELINUX configuration file ensures that the kernel receives the correct IP parameters. Finally, device filesystem support with devfs mounted automatically on boot greatly simplifies the /dev directory in the RAM disk root filesystem.

RAM Disk Root Filesystem

Building and populating the root filesystem would be the most complicated part of setting up a diskless Linux box if it weren't for the advent of Richard Gooch's device filesystem and Erik Anderson's BusyBox combined binary. The device filesystem automatically manages the /dev directory, creating device entry points as needed for the devices loaded in the kernel. This means two things: the directory has no unnecessary entries, and builders of RAM disk root filesystems don't have to spend hours with mknod trying to create all the needed device entry points. The BusyBox combined binary is an executable that changes its personality depending on how it is invoked. The usual methodology is to create symlinks to /bin/busybox from /bin/ls, /bin/cat, /bin/ps, /sbin/mount and so on, to create a minimalist UNIX system. No additional libraries or binaries are needed; the BusyBox rolls them into one.

One way to think of this setup is that the device filesystem takes care of /dev; the BusyBox takes care of /bin and /sbin; the kernel takes care of /proc; a read-only NFS mount takes care of /usr; and /tmp can be empty. So, all you need to worry about is /etc. In fact, /etc can be starkly minimalist, containing only /etc/fstab, /etc/inittab and /etc/init.d/rcS, the latter being the single startup script used by BusyBox when running as init.

BusyBox was written for the world of embedded Linux and normally would be built as a static executable. However, the XFree86 server itself depends on a number of shared libraries normally found in /lib. We are going to NFS-mount /usr, so we don't have to worry about shared libraries found in /usr/lib, but we do have to provide the ones XFree86 expects to find in /lib. Therefore, we might as well take advantage of the space savings made possible by configuring BusyBox as a dynamic executable. The minimum libraries required in /lib to run XFree86 can be discovered by running ldd /usr/X11R6/bin/XFree86. They are glibc (libc.so and libm.so), PAM (libpam.so and libpam_misc.so) and the dynamic loader itself (libdl.so and ld-linux.so).

Configuring XFree86

The XFree86 executable normally is found in /usr/X11R6/bin, a subdirectory of /usr. We don't need to provide the X server in the RAM disk then, but can take it from the NFS mount. Although the modular XFree86 server itself has not been hardware-specific since about version 4.0, its configuration file definitely is. If you are managing several X terminals with different video hardware, it is impossible to use the same XF86Config file for all of them. Therefore, we prefer not to keep it in the RAM disk root filesystem, where it usually would be found in /etc/X11/XF86Config. Instead, we can use a per-terminal configuration file stored in the NFS /usr directory. Ultimately, the BusyBox init process is configured to respawn a shell script continuously containing the single line:

/usr/X11R6/bin/XFree86 \
-xf86config /usr/X11R6/configs/iphex -query \
server

where iphex is the client's IP address in hexadecimal (a naming convention borrowed from PXELINUX) and server is the server's IP address in dotted-decimal. With a few clever awk-on-/proc/cmdline tricks, we can entirely avoid hard coding any hostnames or IP addresses into the RAM disk image.

A basic XFree86 configuration file can be created by running XFree86 -configure on the terminal. In general, this correctly identifies the video hardware, and the resulting configuration file loads the appropriate XFree86 modules. It is worth mentioning, however, that the default pointer device, /dev/mouse, generally doesn't exist on a system using the device filesystem. For example, the PS/2 mouse is found at /dev/misc/psaux instead.

Server-Side Configuration

The part that makes the X terminal an X terminal instead of a Linux box with a graphical display is the -query server part of the XFree86 command line above. This causes the XFree86 server on the terminal to run an XDMCP (X Display Manager Control Protocol) session to try to get the server to manage its display. However, not every server is going to agree to do so.

First, and most obviously, the server must be listening for incoming XDMCP connections. XDMCP is normally on UDP port 177, and most display managers (xdm, gdm, kdm) can be configured to listen for XDMCP requests. Although most distributions are configured to run a display manager on bootup, most do not listen for incoming XDMCP requests due to security considerations. For example, the classic X display manager, xdm, usually is distributed with the line:


DisplayManager.requestPort: 0

in its configuration file (commonly /etc/X11/xdm/xdm-config). This would have to be commented out in order for xdm to accept XDMCP requests. In addition, xdm can be configured to restrict itself to connections on a per-host or per-subnet basis using the configuration file /etc/X11/xdm/Xaccess (don't be confused by /etc/X11/xdm/Xservers, which is largely a historical relic). For example, to restrict xdm to terminals in the 192.168.1.0/24 subnet, add a line containing only 192.168.1.0/24 to the end of /etc/X11/xdm/Xaccess.

In addition, it can be convenient if the server also provides fonts to the terminals, by way of the X font server process xfs. Once again, although most distributions run a font server process, it usually is configured not to listen for incoming requests. For example, the configuration file for xfs, /etc/X11/fs/config, generally contains the line no-listen = tcp. If this is commented out, the Files section of the terminal's XF86Config file (stored in /usr/X11R6/configs/iphex on the server) can contain only one FontPath instead of the usual half-dozen, as shown in Listing 3 (where a server IP of 192.168.1.1 is assumed).

Listing 3. Terminal XF86Config Fragment


Section "Files"
        RgbPath      "/usr/X11R6/lib/X11/rgb"
        ModulePath   "/usr/X11R6/lib/modules"
        FontPath     "tcp/192.168.1.1:7100"
EndSection

Finally, the server must be configured to NFS export its /usr filesystem read-only to the terminal, as this is where the terminal gets the XFree86 server.

Some Words about Security

A number of security considerations should be kept in mind when running X terminals. First, it should be fairly obvious that the changes made to the xdm and xfs configurations are undoing things that were done to increase the security of the server. Furthermore, the setup described in this article does not encrypt any traffic. Every keystroke on the terminal goes over the network unencrypted. The only reasonably safe way to run with X terminals is to put them all on a private LAN that is used only by X terminals and that does not route to the Internet. The terminals and one interface on the server should be the only ones on the terminal LAN.

Kits Are Available

Due to the space limitations of printed media, this article presented a high-level view of how to configure a Linux box to boot diskless and become an X terminal, without going into great detail about the precise implementation. Interested readers are encouraged to download the X Terminal Kit from the author's Web site; it includes shell scripts, Makefiles and READMEs to guide you through the sometimes complicated process. In addition, the software described in this article has been drawn from numerous resources on the Internet, all of which have more detailed information about their particular packages. See the on-line Resources for pointers.

Resources for this article: www.linuxjournal.com/article/7924.

Chip Coldwell (coldwell@physics.harvard.edu) is a system administrator for the Physics Department at Harvard University. When he's not messing around with a computer of some sort, he generally can be found riding his bicycle or enjoying the company of his fiancée, Cindy.

Load Disqus comments

Firstwave Cloud