Rolling your own minimal embedded Linux for the Raspberry Pi -- part three: services and remote access
In this third article in my series on building a custom, minimal Linux for the Raspberry Pi, I describe how to move from a single-user, single-console system, to an installation with multiple consoles and the potential for remote access. Along the way we'll get WiFi working.
This is a long, and rather complicated article, even with a lot of the fine detail omitted for brevity. That's to be expected -- building a complete, custom Linux installation, even a simple one, requires a fair amount of work.
But first, let's recap.
Review
These articles are about building a minimal, fast-booting Linux for embedded applications on the Raspberry Pi (or similar). 'Embedded' is, of course, a vague term; I'm using it here to describe anything that could be considered an appliance, rather than a desktop computer. The main characteristics of an appliance are that should be, so far as possible, instant-on, and instant-off. The 'instant-on' (or, at least, fairly-quickly-on) requirement is met by initializing only minimal services at boot time. The 'instant-off' requirement is met by running with a read-only root filesytem -- one that will not be damaged simply by pulling the plug.
In the first part of this series I described how to create a bootable SD for the Raspberry Pi that would boot to a root shell prompt. The second part described how to set up volatile directories and install additional utilities, leading to a viable single-user Linux system. This additional set-up is necessary because the use of a read-only root filesystem creates constraints that will not be experienced with a desktop system. I pointed out then, and will do so again here, that the purpose of this exercise is to build an appliance that boots quickly and shuts down by pulling the plug. It isn't primarily about minimizing storage. To that end, I described how to create a script that would download Raspberry Pi binaries from Raspian along with their dependencies, and install them in an image of a root filesystem. I gave an example of such a script, and we will need it again in this part of the exercise. Obtaining binaries this way is not storage-efficient, because of the sprawling dependencies of repository-based software; but storage is cheap.
At this stage, you should have a system that boots in a few seconds to a single console with a root prompt. You won't be able to log in as a specific user, because no users exist, and you won't be able to switch virtual consoles. The virtual consoles exist, because they are managed by the kernel; but no process will be connected to them. You may have wired networking set up, because all this needs is a couple of utilities and the support that is built into the kernel; but WiFi is an order of magnitude more complex.
Before going much further, it's necessary to make an architectural decision about service management, because that decision will have far-reaching consequences.
What kind of service manager?
A Linux system will have a certain amount of one-time initialization -- loading kernel modules, mounting filesystems, setting a hostname, etc. We've discussed this already and, in fact, it's not very difficult -- a simple shell script will take care of most of this. However, in addition to one-time initialization, most Linux systems -- even in embedded applications -- will require services: processes that support system operation, and will run in the background so long as the system does.
What sorts of services will need to be started? That's impossible to answer in general terms, because it depends what the system is going to do. It's tempting to think that, in an embedded application, you don't need any services at all. However, you might need some or all of the following, if only for development and debugging:
A logging daemon. Most applications need some way to log operations, and a way for administrators or developers to view those logs. In a read-only filesystem, traditional file-based loggers are no use. More on this point later.
A remote log-in service --
telnetd
orsshd
. In practice, the latter is more likely to be used these days, because of security concerns.Something to associate consoles -- real or virtual -- with user sessions --
getty
or one of its light-weight alternatives.The infrastructure needed to maintain an encrypted WiFi network link (
wpa_supplicant
or similar)
In the desktop/server Linux world, there are two main strategies for
service management:
the old SysV-style init
process, and
the more modern systemd
. Almost all mainstream Linux
distributions now use systemd
, for better or worse. It's a much
more grown-up strategy than SysV init, because it manages boot-time dependencies;
but it's overwhelmingly too complicated for any kind of appliance.
In the embedded Linux world, the choice is usually between one of the following:
Nothing at all. Linux boots directly to an application, which controls everything from then on. We already know how to do that -- just tell the kernel that the application (which could be a script) is the system's
init
process. This is almost certainly the right way to do things in a production appliance, but it's only practicable when you're at the stage where you really only need the most minimal set of services.The traditional
init
. This is still available in the Raspbian repositories, and is well-understood.Bespoke. Implement your own service management framework.
It's certainly possible to code or script something that will start a
few services and keep them running. However, I would argue that
SysV init
scripts are a better approach, even in an embedded system.
init
uses minimal memory, and isn't noticeably slower in
operation than a hand-crafted solution. In the rest of this article
I will assume that init
will be the service manager, although
the basic principles even if something else is used.
Installing and using SysV init in an embedded system
SysV init is in the Raspbian repository, under the name
sysvinit-core
. This package provides the init
binary itself, a bunch of helper scripts and binaries, and a
a selection of (mostly unhelpful in this situation)
startup scripts.
Installing the init
infrastructure
To use init
you will need
the following.
(1) The init
binary itself, and a way for the kernel to run
it. Running /sbin/init
is, in fact, the Linux kernel's default
boot mode
but, if you've followed the previous articles, you may have changed that
default by editing cmdline.txt
. To use init
,
the init
binary needs to be in the /sbin/
directory, and
there needs to be no conflicting setting of init=...
in
cmdline.txt
.
(2) The main configuration file for init
, which is /etc/inittab
. For an example, see below.
(3) A way to create the various scripts you need to initialize the system.
(4) If you want init
to manage virtual
consoles, you'll need a getty
implementation. There are at least a half dozen available but, unless
you need to handle real serial terminals, it's probably easiest to use
agetty
from the util-linux
package. The
getty
binary is conventionally installed in
/sbin
. Installing util-linux
will also get you
/bin/login
, which you'll need to authenticate users.
The iniitab file
This file, /etc/inittab
, is read by init
at
boot time, and then used to coordinate all the other service management
activities. There was a time when it would not have been necessary
to explain how init
worked to any experienced Linux
user. These days, with the widespread adoption of systemd
,
a brief explanation might be in order.
init
has a concept of run levels. Traditionally, a
Linux system could, in principle,
progress through the various run levels at it starts
up, with run level 1 being single user mode, and run level 5 being
full multi-user operation with graphical desktop. I would argue that
this complexity is unnecessary in an embedded system -- there are really
only two run levels: "on" and "off", which I denote by "0" and "1", since
they have to be numeric.
Here is a sample inittab
file, for an installation that
has only two runlevels.
si::sysinit:/etc/rc.d/startup.sh id:1:initdefault: 1:1:respawn:/sbin/getty tty1 2:1:respawn:/sbin/getty tty2 3:1:respawn:/sbin/getty tty3 l0:0:wait:/etc/init.d/rc 0 l1:1:wait:/etc/init.d/rc 1 sd::shutdown:/etc/rc.d/shutdown ca::ctrlaltdel:/sbin/reboot
The si:sysinit
line indicates a script to run to perform
one-time initialization. This script is the place to mount
volatile file systems, set the hostname, etc -- things I described
in the previous article in this series.
The initdefault
entry indicates the run level to enter
by default. Since there is only one run level other than "off", there
isn't a lot of choice here.
The sd
and ca
lines define shutdown and
reboot behaviour but, since this system will shut down by pulling the
plug, these settings aren't particularly important.
The lines '1', '2', etc., indicate that getty
should
be executed in run level 1, and that it should continue to execute.
The respawn
setting is important here, because
getty
shuts down when a user has logged in. In order for
a console to continue to be useful, getty
has to be
restarted when the virtual console has no process attached.
The r1
line indicates that init
should
run the rc
script on the directory /etc/rc1.d
at startup and shutdown (strictly speaking, when entering
and leaving run level 1, but that amounts to the same thing in this
configuration). The rc.1
directory will contain scripts with names
'SNNsomething' and `KNNsomething`, where the NN are numbers.
The 'S' scripts are run in numerical order when entering run level
1 -- that is, at start-up; the 'K' scripts are
run in reverse numerical order at shutdown.
In practice, the 'S' script and the 'K' script with a particular
number can be links to the same script -- the rc
process will
run the script with the argument start
at startup
and stop
at shutdown. It's conventional to make
both the S and K scripts symbolic links to the 'real' scripts, which
are in the directory /etc/init.d
. This allows the
start and stop code to be maintained in the same file, rather than
two separate files.
When a system had many
run levels, and many different processes that had to be started
and stopped when moving between run levels, some organization of this
sort was essential. There is no actual harm in employing
the same organization in an embedded system, and following the
convention makes the start-up behaviour easier to follow than
it might otherwise be.
A sample init script
Here is a very simple script for starting and stopping sshd
;
I would store this script as /etc/init.d/sshd
.
#!/bin/sh case "$1" in stop) killall sshd ;; start) mkdir /run/sshd /usr/sbin/sshd -h /etc/ssh/id_rsa ;; *) echo 'Usage: /etc/init.d/sshd {start|stop}' exit 3 ;; esacThis script will be linked to an 'S' file and a 'K' file in
rc1.d
.
From the running system, I see this:
$ cd /etc/rc1.d/ kevin@pi:/etc/rc1.d$ ls -l *sshd* lrwxrwxrwx 1 root root 14 Dec 11 11:41 K40sshd -> ../init.d/sshd lrwxrwxrwx 1 root root 14 Dec 11 11:41 S40sshd -> ../init.d/sshd kevin@pi:/etc/rc1.d$
At startup, init
runs S40sshd
with argument
start
, which runs /usr/bin/sshd
with
some necessary arguments. At shutdown, init
runs
killall sshd
to stop sshd
. This is pretty crude,
and only really justifiable since the system will never actually
go through an orderly shutdown.
Even without installing any init scripts, starting the system using init
will make a difference -- rather than booting to a root shell, the
system will now boot to a 'login:' prompt, provided by getty
.
Unfortunately, you won't be able to log in at the point, because you
won't have any users to log in as -- see below for how to fix that.
Having explained how init
runs scripts, we can look at how
to use scripts to start various system services.
System log daemon
Most Linux systems will need a log daemon. The daemon traditionally listens
for connections on a specific Unix socket, and writes data supplied by
applications to a file /var/log/syslog
. The log file
may rotate when it gets too large.
This form of logging is not very appropriate in an embedded system, particularly
with a read-only root filesystem. The directory /var/log
can be
made writable by, for example, linking it to a temporary filesystem in RAM.
However, this isn't a very good use of limited RAM. What's required is a
log daemon that maintains a fixed-size buffer in memory, and flushes
the buffer out to a file (in a RAM filesystem) at periodic intervals.
busybox
package contains an implementation of a log
daemon with a fixed size buffer, but it needs a specific utility to read
the log, which I find a little inconvenient. So I wrote my own version,
syslogd-lite
, which is
on GitHub.
syslogd-lite
looks like a traditional log daemon to applications
that write logs, and the /var/log/syslog
file looks like
an ordinary file to administrators, except that it will never exceed
a fixed size.
All the various lightweight log daemons can be started at boot time simply
by running syslogd
. There is no particular reason to provide
an orderly way to stop them -- any data they are storing will disappear at
shutdown anyway, since it's in RAM.
Users
At present, our Linux has no users. Although I've been talking about "booting to
a root shell" in fact, unless the /etc/passwd
file exists,
we don't even have root
as a user. If a system is to allow
remote access, it makes sense to have at least one unprivileged user
account to do it as. In fact, that makes sense even if the system
doesn't allow remote access -- working as root
as
a matter of routine just feels icky.
If you set up init
with getty
as described above,
then you should be able to see a log-in prompt in the various
virtual consoles. However, to be able to log in, you need as a minimum a
passed
file. This should define at least two users --
root, and at least one unprivileged user. If you're going to allow remote access
using ssh
, then you'll need another user just for sshd
,
but more on that later.
A minimal passwd
file for an embedded system might look
like this:
root:x:0:0:root:/root:/bin/false user:[long encrypted password]:1000:100:User:/home/user:/bin/bash
In a Linux server installation it has become common practice to avoid
putting passwords -- even encrypted passwords -- into passwd
.
These now go into a separate file, /etc/shadow
, which
is not world-readable. I don't feel that this complexity is necessary
in an embedded system, but there's nothing to stop you setting things up
that way if you prefer. Wherever you put the encrypted passwords, there's
the problem of generating them.
A simple solution is just to create a user with the required username on some other, desktop Linux system, and just cut-and-paste the encrypted password between the systems. Nothing system-specific is encoded into the encrypted password.
Note that I've disallowed logging in as root
completely.
I will log in as user
, and use sudo
to change
user if necessary. Arguably, this additional complexity is not necessary
in an embedded system, but setting up sudo
is simple
enough. More on that later, though.
Along with a simple passwd
file, you'll also need an
/etc/group
file to define groups for the users in
passwd
. It's trivially simple at this stage:
root:x:0: users:x:100:
Note that my passwd
file defines the home directory of
user
as /home/user
. This directory will
need (ideally) to be created, and set to the ownership of the
user
user. If it's missing it won't prevent you logging
in, but you'll have no place with permissions to store files, and you'll get a lot
of irritating error messages.
At this stage -- with init
and getty
installed
and set up, you should be able to log in as user
at
the login prompt, and you should be able to do this on multiple virtual
consoles, switching between them using ctrl+alt+F1, ctrl+alt+F2, etc.
sudo
Since we have provided no way for root
to log in, we need
to enable either su
or sudo
so that
the user can administer the system. I should point out that only
limited administration is possible, because the root filesystem
is read-only. I describe later how to switch the root filesystem between
read-only and read-write modes.
sudo
is in the Raspbian repository in the package
sudo
. This package includes not only the binary, but
also a sample /etc/sudoers
file. In the present case,
you can probably delete the sample and replace it with this:
root ALL=(ALL) ALL user ALL=(ALL) ALLWith this change in place, you should be able to log in as
user
, and do
$ sudo -i
to get a root shell. There's nothing to stop you adding additional
user accounts at this stage, by editing /etc/password
,
/etc/group
, and /etc/sudoers
manually --
whether you need to do so depends, of course, on your application.
Switching between read-only and read-write filesystem
At present, the root filesystem is read-only and, if you're building a Linux appliance, I would expect it to stay that way through the whole production lifetime of the system. However, sometimes it is helpful to carry out experiments on the Pi itself, rather than building a new SD card image for each simple change.
Note:
If you switch to a read-write filesystem, it's all too easy to make changes to the live system and then not carry them back to your build environment. Then, when you build the SD card image again, the changes are overwritten. I don't know any solution to this problem except rigorous self-discipline. If you lack this, as I do, I would advise not switching to read-write mode often, if at all
In the previous article in this series
I describe how to create a rudimentary /etc/fstab
file.
One of the entries in that file:
/dev/mmcblk0p2 / auto defaults,noatime,ro 0 1may not have had a clear purpose at that time. However, with this entry in place, you can now switch the root filesystem to read-write at the prompt:
$ sudo mount -o rw,remount /and to switch it back to read-only:
$ sudo mount -o ro,remount /
This can be done when process are running, and have open files on the filesystem, if necessary.
Note:
Whenever the root filesystem is in read-write mode, it is vulnerable to damage if there is a disorderly shut-down or power-off. I would recommend switching back to read-only before switching off. Or just not using read-write mode at all, if practicable
WiFi networking
In the previous article I described
how to obtain basic networking utilities and set a hostname. If you
know how to use ipconfig
you should be able to get a
wired network connection up, at least with a fixed IP number.
If you have an ethernet cable and a switch/router to plug it into,
I would certainly recommend trying wired networking
at this stage. Getting encrypted
WiFi working (and it's always encrypted these days) is more complicated,
and its worth checking that you know how to make wired networking
operate first.
In this section I will explain the minimum needed to make WPA-encrypted WiFi work, with a static IP number and configuration. For dynamic configuration you would also need to install a DHCP client daemon, which isn't difficult, but that's a job for another day.
Prerequisites
You will need:
The
iw
andwpasupplicant
packages from the Raspbian repositoryThe SSID of an access point on the WiFi network and the corresponding key (password)
An IP number you can assign to your Pi, in the range the access point supports
The IP number of a default gateway (may be your DSL router)
The IP number of at least one DNS server (may be your DSL router)
You'll need to know the name of the kernel modules that support the WiFi adapter. These aren't easy to guess, but a web search for the board model and "wifi kernel module" will probably get the information, with a bit of digging. On the Pi 3B+, the relevant modules are
brcmutil
andbrcmfmac
Starting wpa_supplicant
wpa_supplicant
is the process that negotiates encryption
in the background. You'll need it for WiFi operation on any network
that uses encryption.
wpa_supplicant
is started with the name of a network interface
(usually wlan0
, and a configuration file. The configuration
file needs as a minimum to specify the location of the 'control interface'
-- a directory where it can write a socket. This directory must exist,
and be writeable; since we're working on a read-only filesystem,
the script that starts wpa_supplicant
will need to
create this directory in some writeable area (that is, on a temporary
filesytem). With these facts in mind, here is an init script to start
and stop
wpa_supplicant
:
#!/bin/sh if [ -f /sbin/wpa_supplicant ]; then case "$1" in stop) killall wpa_supplicant ;; start) modprobe brcmutil modprobe brcmfmac mkdir -p /var/run/wpa_supplicant sleep 1 wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant/wpa_supplicant.conf ;; *) echo 'Usage: /etc/init.d/wpa_supplicant {start|stop}' exit 3 ;; esac fi
The rather irritating one-second sleep is needed to give the kernel drivers
time to initialize before starting wpa_supplicant
.
One second is probably far too long -- you might be able to tune this to
a shorter value, or have the initialization of wpa_supplicant
simply repeat until it succeeds.
The corresponding configuration file wpa_supplicant.conf
is
simply this:
ctrl_interface=DIR=/var/run/wpa_supplicant
Setting up the WiFi adapter
Once wpa_supplicant
is running, we can initialize
the WiFi adapter, using static settings. Here is a sample init script:
#!/bin/sh case "$1" in stop) ifconfig wlan0 down ;; start) wpa_cli -i wlan0 add_network wpa_cli -i wlan0 set_network 0 ssid [MY_SSID] wpa_cli -i wlan0 set_network 0 psk [MY_KEY] wpa_cli -i wlan0 enable_network 0 ifconfig wlan0 $IP up route add default gw [MY_GATEWAY] ;; *) echo 'Usage: /etc/init.d/wifi {start|stop}' exit 3 ;; esac
Note:
You'll find that network-related properties appear in many different configuration files as the system builds up, and it's better to store these in some sort of centralized configuration file, than hard-code them in a half-dozen different scripts. More on this point later.
If you want to get network access beyond the local network, you'll need
to ensure you have a DNS server in /etc/resolv.conf
nameserver [MY_DNS_SERVER] ...
Running the scripts to start wpa_supplicant
and the
WiFi adapter should get you a working wireless network connection.
If it doesn't, some troubleshooting will be in order. It's at
this point where, if you haven't yet installed a system log daemon,
you'll be regretting that decision, as all the diagnostic output from
wpa_supplicant
and wpa_cli
goes to
the system log.
Date and time
Not all applications will require correct date and time but, if yours does, you'll either have to buy/build a real-time-clock module, or get the system time from a network source.
The conventional way to synchronise time is to use an NTP daemon,
although many Linux distributions have their own, usually simpler
methods. An easy way to set the time on a one-off basis when
the system starts is to make a request on a reliable webserver
using curl
(package curl
in the
Raspbian repository) and parse out the date from the response.
Here is a script to do this.
#!/bin/bash URL=http://google.com DELAY=5 tries=0 notset=true while $notset && (( $tries < $MAX_TRIES )) do logger --socket-errors=off -s $0: Getting time from $URL response=`curl -s --head $URL | grep ^Date: | sed 's/Date: //g'` if [ -z "$response" ]; then logger --socket-errors=off -s $0: No response -- sleeping sleep $DELAY tries=$(($tries+1)) else notset=false date -s "$response" > /dev/null fi done if (( $tries == $MAX_TRIES )); then logger --socket-errors=off -s \ $0: Could not get date from $URL after $MAX_TRIES attempts -- giving up fiNote that this script uses
logger
to write to the system
log -- this utility is part of the util-linux
package.
ssh client and server
Once networking is working, making outbound SSH connections from the Pi
is straightforward enough -- just use the ssh
utility from
the openssh-client
package. Installing an SSH server,
to accept incoming connections, is a bit more fiddly -- and probably
a lot more useful, in an embedded application.
The sshd
daemon is in the openssh-client
package in the Raspbian repository. In principle, running it amounts
to nothing more than starting the process. Here is a simple init
script to do that:
#!/bin/sh case "$1" in stop) killall sshd ;; start) mkdir /run/sshd /usr/sbin/sshd -h /etc/ssh/id_rsa ;; *) echo 'Usage: /etc/init.d/sshd {start|stop}' exit 3 ;; esac
There are a few things to note. As with many services, there needs to
be a writable directory to store temporary files. In this case, it will
be /run/sshd
. The script will need to create this and,
as ever, it will need to be in a temporary filesystem.
The openssh-server
package provides various configuration
files, some of which might need to be edited, to enable or disable
particular features.
Although it isn't clear from the set-up, sshd
needs
to run as a particular user for security purposes, even though it
starts as root
. So we need to add to /etc/passwd
:
sshd:!:50:33:sshd:/:/bin/bash
This user need not be able to log in, and the account won't own any files, but it needs to exist.
The final, and most fiddly, problem is that sshd
needs
a server certificate. In the script above it was specified as
/etc/ssh/id_rsa
. Generating a simple, self-signed
certificate is easy enough:
$ ssh-keygen -b 1024 -t rsa -f /path/to/id_rsa
But this can't be done on the Pi -- not with a read-only filesystem, anyway. The certificate generation process is slow on a Pi -- it will take at least as long as the complete boot process so far. Because it's a read-only filesystem, we can't generate the certificate on the first boot, and then store it -- because there is nowhere to store it.
Instead, the certificate will need to be generated on the build host, when the root filesystem is being assembled, and then copied to the staging root filesystem for eventual transfer to an SD card.
Summary
The preceding sections have described various services that are likely to be needed when building up from a system that boots to single-user mode, to a full multi-user system with remote access.
On a Pi 3B+ -- which isn't the fastest single-board computer
on earth -- the total cold boot time to a login prompt is about seven
seconds. About five seconds of this is firmware and kernel initialization,
which we have no control over, other than building a custom
kernel.
Once the system is started, I have the following processes
running (plus init
, which doesn't appear in this list):
user@pi:~$ ps -ef|grep -v \\[ UID PID PPID C STIME TTY TIME CMD root 91 1 0 12:27 ? 00:00:00 syslogd root 94 1 0 12:27 tty1 00:00:00 /sbin/getty tty1 root 95 1 0 12:27 tty2 00:00:00 /sbin/getty tty2 root 96 1 0 12:27 tty3 00:00:00 /sbin/getty tty3 root 126 1 0 12:27 ? 00:00:00 wpa_supplicant -B -i wlan0 -c /wpa_supplicant.conf root 148 1 0 12:27 ? 00:00:00 /usr/sbin/sshd -h /etc/ssh/id_rsa user 173 162 0 12:28 ? 00:00:00 sshd: user@pts/0
It's interesting to consider what isn't running on what is, after all, now a workable, multi-user console workstation:
systemd
and all its sub-processesdbus
udev
exim4
In other words, all the heavyweights of the standard Raspbian distribution (not including all the graphical desktop stuff, of course). Directly after boot, of the 1Gb or so of RAM on the Pi 3B+, 256Mb is used for the temporary filesystem in RAM, and 21Mb is used by the various processes. All the rest is free, or available for page caching.
This looks a lot more like the process and memory picture of an embedded Linux system than even the lightest-weight mainstream Raspberry Pi distribution does.
A note on configuration
A topic I have skimmed over in these articles so far is that of centralizing configuration. I'm assuming that the custom Linux installation will be used for lightweight applications, and might not have any user interface. Even if there is a simple user interface -- command-line console, for example -- the read-only nature of the installation makes configuration awkward. My intention is that all the necessary configuration is burned into the image on SD card at build time.
Since I will be using the same base image in a number of different installations, I need a way to customize the installation for each build. Parameters that will need to be changed include:
Network settings
Hostname
User credentials
Kernel modules to be loaded
There is not normally a centralized configuration database for Linux, and these settings end up spread through various files on the system. Modifying the settings for multiple systems becomes a real problem.
My approach to configuration management -- which would only work on a simple, embedded Linux system -- is to have on the root filesytem one single configuration file that externalizes all the system-dependent settings into one file. All the init scripts, etc., load this file when they start, and use the settings in it. Nothing is hard-coded into any script.
Consequently, I can use the same SD card image for a number of different systems, and only have to edit a single file on the card to set it up. It takes a lot of extra work, and a good dose of self-discipline, to set things up this way, but it pays off in the long run.
Further work
In this article I've described how to set up init
from scratch, and use it to manage services that I think many small Linux
systems will need. However, that isn't the end of the job. Many
systems will need some or all of the following.
cron
, or some other way to run processes at particular timesFor long-running systems, a time-synchronising process like NTP
Software firewall configuration could become more significant if the system increases in complexity
A DHCP client daemon for automatic network configuration
And, of course, you'll need some software that actually does some useful work, rather than just keeping the system running.
Next: audio; or go to the series index.