1
0

convert rst to asciidoc

for i in *.rst ; do pandoc -f rst -t asciidoc -o `basename $i .rst`.adoc $i ;done
This commit is contained in:
Daniel Barlow 2025-04-06 18:16:53 +01:00
parent 936eb1a7cd
commit de861a2ee0
17 changed files with 1689 additions and 1783 deletions

352
doc/admin.adoc Normal file
View File

@ -0,0 +1,352 @@
== System Administration
=== Services on a running system
Liminix services are built on s6-rc, which is itself layered on s6.
Services are defined at build time in your configuration (see
`+configuration-services+` for information) and can't be added
to/changed at runtime, but to monitor events or diagnose problems you
may need to inspect them on the running system. Here are some of the
most commonly used s6,-rc commands:
.Service management quick reference
[width="100%",cols="55%,45%",options="header",]
|===
|What |How
|List all running services |`+s6-rc -a list+`
|List all services that are *not* running |`+s6-rc -da list+`
|List services that `+wombat+` depends on
|`+s6-rc-db dependencies wombat+`
|... transitively |`+s6-rc-db all-dependencies wombat+`
|List services that depend on service `+wombat+`
|`+s6-rc-db -d dependencies wombat+`
|... transitively |`+s6-rc-db -d all-dependencies wombat+`
|Stop service `+wombat+` and everything depending on it
|`+s6-rc -d change wombat+`
|Start service `+wombat+` (but not any services depending on it)
|`+s6-rc -u change wombat+`
|Start service `+wombat+` and all* services depending on it
|`+s6-rc-up-tree wombat+`
|===
`+s6-rc-up-tree+` brings up a service and all services that depend on
it, except for any services that depend on a "controlled" service that
is not currently running. Controlled services are not started at boot
time but in response to external events (e.g. plugging in a particular
piece of hardware) so you probably don't want to be starting them by
hand if the conditions aren't there.
A service may be *up* or *down* (there are no intermediate states like
"started" or "stopping" or "dying" or "cogitating"). Some (but not all)
services have "readiness" notifications: the dependents of a service
with a readiness notification won't be started until the service signals
(by writing to a nominated file descriptor) that it's prepared to start
work. Most services defined by Liminix also have a `+timeout-up+`
parameter, which means that if a service has readiness notifications and
doesn't become ready in the allotted time (defaults 20 seconds) it will
be terminated and its state set to *down*.
If the process providing a service dies, it will be restarted
automatically. Liminix does not automatically set it to *down*.
(If the process providing a service dies without ever notifying
readiness, Liminix will restart it as many times as it has to until the
timeout period elapses, and then stop it and mark it down.)
==== Controlled services
*Controlled* services are those which are started/stopped on demand by a
*controller* (another service) instead of being started at boot time.
For example:
* `+svc.uevent-rule.build+` creates a controlled service which is active
when a particular hardware device (identified by uevent/sysfs directory)
is present.
* `+svc.round-robin.build+` creates a service controller that invokes
two or more services in turn, running the next one when the process
providing the previous one exits. We use this for failover from one
network connection to a backup connection, for example.
* `+svc.health-check.build+` creates a service controller that runs a
controlled service and periodically tests whether it is healthy by
running an external health check command or script. If the check command
repeatedly fails, the controlled service is restarted.
+
The Configuration section of the manual describes controlled services in
more detail. Some operational considerations
* `+round-robin+` detects a service status by looking at its `+outputs+`
directory, so it won't work unless the service creates some outputs.
This is considered a bug and will be fixed in a future release
* `+health-check+` works for longruns but not for oneshots, as it
internally relies on `+s6-svc+` to restart the process
==== Logs
Logs for all services are collated into `+/run/log/current+`. The log
file is rotated when it reaches a threshold size, into another file in
the same directory whose name contains a TAI64 timestamp.
Each log line is prefixed with a TAI64 timestamp and the name of the
service, if it is a longrun. If it is a oneshot, a timestamp and the
name of some other service. To convert the timestamp into a
human-readable format, use `+s6-tai64nlocal+`.
[source,console]
----
# ls -l /run/log/
-rw-r--r-- 1 0 lock
-rw-r--r-- 1 0 state
-rwxr--r-- 1 98059 @4000000000025cb629c311ac.s
-rwxr--r-- 1 98061 @40000000000260f7309c7fb4.s
-rwxr--r-- 1 98041 @40000000000265233a6cc0b6.s
-rwxr--r-- 1 98019 @400000000002695d10c06929.s
-rwxr--r-- 1 98064 @4000000000026d84189559e0.s
-rwxr--r-- 1 98055 @40000000000271ce1e031d91.s
-rwxr--r-- 1 98054 @400000000002760229733626.s
-rwxr--r-- 1 98104 @4000000000027a2e3b6f4e12.s
-rwxr--r-- 1 98023 @4000000000027e6f0ed24a6c.s
-rw-r--r-- 1 42374 current
# tail -2 /run/log/current
@40000000000284f130747343 wan.link.pppoe Connect: ppp0 <--> /dev/pts/0
@40000000000284f230acc669 wan.link.pppoe sent [LCP ConfReq id=0x1 <asyncmap 0x0> <magic 0x667a9594> <pcomp> <accomp>]
# tail -2 /run/log/current | s6-tai64nlocal
1970-01-02 21:51:45.828598156 wan.link.pppoe sent [LCP ConfReq id=0x1 <asyncmap 0x0> <magic 0x667a9594> <pcomp> <accom
p>]
1970-01-02 21:51:48.832588765 wan.link.pppoe sent [LCP ConfReq id=0x1 <asyncmap 0x0> <magic 0x667a9594> <pcomp> <accom
p>]
----
===== Log persistence
Logs written to `+/run/log/+` will not survive a reboot or crash, as it
is an ephemeral filesystem.
On supported hardware you can enable logging to
https://www.kernel.org/doc/Documentation/ABI/testing/pstore[pstore]
which means the most recent log messages will be preserved on reboot.
Set the config option `+logging.persistent.enable = true+` to log
messages to `+/dev/pmsg0+` as well as to the regular log. This is a
circular buffer, so when it fills up newer messages will overwrite the
oldest messages.
Logs found in pstore after a reboot will be moved at startup to
`+/run/log/previous-boot+`
=== Updating an installed system
If your system has a writable root filesystem (JFFS2, btrfs etc
-anything but squashfs), we have mechanisms for in-places updates
analogous to `+nixos-rebuild+`, but the operation is a bit different
because it expects to run on a build machine and then copy to the host
device using `+ssh+`.
To use this, build the `+outputs.updater+` target and then run the
`+update.sh+` script it generates.
[source,console]
----
nix-build -I liminix-config=./my-configuration.nix \
--arg device "import ./devices/mydevice" \
-A outputs.updater
./result/bin/update.sh root@the-device
----
The update script uses min-copy-closure to copy new or changed packages
to the device, then (perhaps) reboots it. The reboot behaviour can be
affected by flags:
* [.title-ref]#--no-reboot# will cause it not to reboot at all, if you
would rather do that yourself. Note that none of the newly-installed or
updated services will be running until you do.
* [.title-ref]#--fast# causes it tn not do a full reboot, but instead to
restart only the services that have been changed. This will restart all
of the services that have updated store paths (and anything that depends
on them), but will not affect services that haven't changed.
It doesn't delete old packages automatically: to do that run
`+min-collect-garbage+`, which will delete any packages not in the
current system closure. Note that Liminix does not have the NixOS
concept of environments or generations, and there is no way back from
this except for building the previous configuration again.
==== Caveats
* it needs there to be enough free space on the device for all the new
packages in addition to all the packages already on it - which may be a
problem if there is little flash storage or if a lot of things have
changed (e.g. a new version of nixpkgs).
* it may not be able to upgrade the kernel: this is device-dependent. If
your device boots from a kernel image on a raw MTD partition or or UBI
volume, update.sh is unable to alter the kernel partition. If your
device boots from a kernel inside the filesystem (e.g. using
bootloader.extlinux or bootloder.fit) then the kernel will be upgraded
along with the userland
==== Recovery/downgrades
The `+update.sh+` script also creates a timestamped symlink on the
device which points to the system configuration it installs. If you
install a configuration that doesn't work, you can revert to any other
installed configuration by
[arabic]
. booting to some kind of rescue or recovery system (which may be some
vendor-provided rescue option, or your own recovery system perhaps based
on `+examples/recovery.nix+`) and mounting your Liminix filesystem on
/mnt
. picking another previously-installed configuration that _link:[did]
work, and switching back to it:
[source,console]
----
# ls -ld /mnt/*configuration
lrwxrwxrwx 1 90 /mnt/20252102T182104.configuration -> nix/store/v1w0h4zw65ah4c2r0k7nyy125qrxhq78-system-configuration-aarch64-unknown-linux-musl
lrwxrwxrwx 1 90 /mnt/20251802T181822.configuration -> nix/store/wqjl9s9xljl2wg8257292zghws9ssidk-system-configuration-aarch64-unknown-linux-musl
# : 20251802T181822 is the working system, so reinstall it
# /mnt/20251802T181822.configuration/bin/install /mnt
# umount /mnt
# reboot
----
This will install the previous configuration's activation binary into
/bin, and copy its kernel and initramfs into /boot. Note that it depends
on the previous system not having been garbage-collected.
[[levitate]]
==== Adding packages
If you simply wish to add a package without any change to services, you
can call `+min-copy-closure+` directly to install any package in Nixpkgs
or in the Liminix overlay
[source,console]
----
nix-build -I liminix-config=./my-configuration.nix \
--arg device "import ./devices/mydevice" -A pkgs.tcpdump
nix-shell -p min-copy-closure root@the-device result/
----
Note that this only copies the package and its dependencies to the
device: it doesn't update any profile to add it to `+$PATH+`
[[rebuilding the system]]
=== Reinstalling on a running system
Liminix is initially installed from a monolithic `+firmware.bin+` - and
unless you're running a writable filesystem, the only way to update it
is to build and install a whole new `+firmware.bin+`. However, you
probably would prefer not to have to remove it from its installation
site, unplug it from the network and stick serial cables in it all over
again.
It is not (generally) safe to install a new firmware onto the flash
partitions that the active system is running on. To address this we have
`+levitate+`, which a way for a running Liminix system to "soft restart"
into a ramdisk running only a limited set of services, so that the main
partitions can then be safely flashed.
==== Configuration
Levitate _needs to be configured when you create the initial system_ to
specify which services/packages/etc to run in maintenance mode. Most
likely you want to configure a network interface and an ssh for example
so that you can login to reflash it.
[source,nix]
----
defaultProfile.packages = with pkgs; [
...
(levitate.override {
config = {
services = {
inherit (config.services) dhcpc sshd watchdog;
};
defaultProfile.packages = [ mtdutils ];
users.root = config.users.root;
};
})
];
----
==== Use
Connect (with ssh, probably) to the running Liminix system that you wish
to upgrade.
[source,console]
----
bash$ ssh root@the-device
----
Run `+levitate+`. This takes a little while (perhaps a few tens of
seconds) to execute, and copies all config required for maintenance mode
to `+/run/maintenance+`.
[source,console]
----
# levitate
----
Reboot into maintenance mode. You will be logged out
[source,console]
----
# reboot
----
Connect to the device again - note that the ssh host key will have
changed.
[source,console]
----
# ssh -o UserKnownHostsFile=/dev/null root@the-device
----
Check we're in maintenance mode
[source,console]
----
# cat /etc/banner
LADIES AND GENTLEMEN WE ARE FLOATING IN SPACE
Most services are disabled. The system is operating
with a ram-based root filesystem, making it safe to
overwrite the flash devices in order to perform
upgrades and maintenance.
Don't forget to reboot when you have finished.
----
Perform the upgrade, using flashcp. This is an example, your device will
differ
[source,console]
----
# cat /proc/mtd
dev: size erasesize name
mtd0: 00030000 00010000 "u-boot"
mtd1: 00010000 00010000 "u-boot-env"
mtd2: 00010000 00010000 "factory"
mtd3: 00f80000 00010000 "firmware"
mtd4: 00220000 00010000 "kernel"
mtd5: 00d60000 00010000 "rootfs"
mtd6: 00010000 00010000 "art"
# flashcp -v firmware.bin mtd:firmware
----
All done
[source,console]
----
# reboot
----

View File

@ -1,371 +0,0 @@
System Administration
#####################
Services on a running system
****************************
Liminix services are built on s6-rc, which is itself layered on s6.
Services are defined at build time in your configuration (see
:ref:`configuration-services` for information) and can't be added
to/changed at runtime, but to monitor
events or diagnose problems you may need to inspect them on the
running system. Here are some of the most commonly used s6,-rc
commands:
.. list-table:: Service management quick reference
:widths: 55 45
:header-rows: 1
* - What
- How
* - List all running services
- ``s6-rc -a list``
* - List all services that are **not** running
- ``s6-rc -da list``
* - List services that ``wombat`` depends on
- ``s6-rc-db dependencies wombat``
* - ... transitively
- ``s6-rc-db all-dependencies wombat``
* - List services that depend on service ``wombat``
- ``s6-rc-db -d dependencies wombat``
* - ... transitively
- ``s6-rc-db -d all-dependencies wombat``
* - Stop service ``wombat`` and everything depending on it
- ``s6-rc -d change wombat``
* - Start service ``wombat`` (but not any services depending on it)
- ``s6-rc -u change wombat``
* - Start service ``wombat`` and all* services depending on it
- ``s6-rc-up-tree wombat``
:command:`s6-rc-up-tree` brings up a service and all services that
depend on it, except for any services that depend on a "controlled"
service that is not currently running. Controlled services are not
started at boot time but in response to external events (e.g. plugging
in a particular piece of hardware) so you probably don't want to be
starting them by hand if the conditions aren't there.
A service may be **up** or **down** (there are no intermediate states
like "started" or "stopping" or "dying" or "cogitating"). Some (but
not all) services have "readiness" notifications: the dependents of a
service with a readiness notification won't be started until the
service signals (by writing to a nominated file descriptor) that it's
prepared to start work. Most services defined by Liminix also have a
``timeout-up`` parameter, which means that if a service has readiness
notifications and doesn't become ready in the allotted time (defaults
20 seconds) it will be terminated and its state set to **down**.
If the process providing a service dies, it will be restarted
automatically. Liminix does not automatically set it to **down**.
(If the process providing a service dies without ever notifying
readiness, Liminix will restart it as many times as it has to until the
timeout period elapses, and then stop it and mark it down.)
Controlled services
===================
**Controlled** services are those which are started/stopped on demand
by a **controller** (another service) instead of being started at boot
time. For example:
* ``svc.uevent-rule.build`` creates a controlled service which is
active when a particular hardware device (identified by uevent/sysfs
directory) is present.
* ``svc.round-robin.build`` creates a service controller that
invokes two or more services in turn, running the next one when the
process providing the previous one exits. We use this for failover
from one network connection to a backup connection, for example.
* ``svc.health-check.build`` creates a service controller that
runs a controlled service and periodically tests whether it is
healthy by running an external health check command or script. If the
check command repeatedly fails, the controlled service is
restarted.
The Configuration section of the manual describes controlled
services in more detail. Some operational considerations
* ``round-robin`` detects a service status by looking at its
:file:`outputs` directory, so it won't work unless the service
creates some outputs. This is considered a bug and will be
fixed in a future release
* ``health-check`` works for longruns but not for oneshots, as it
internally relies on ``s6-svc`` to restart the process
Logs
====
Logs for all services are collated into :file:`/run/log/current`.
The log file is rotated when it reaches a threshold size, into another
file in the same directory whose name contains a TAI64 timestamp.
Each log line is prefixed with a TAI64 timestamp and the name of the
service, if it is a longrun. If it is a oneshot, a timestamp and the
name of some other service. To convert the timestamp into a
human-readable format, use :command:`s6-tai64nlocal`.
.. code-block:: console
# ls -l /run/log/
-rw-r--r-- 1 0 lock
-rw-r--r-- 1 0 state
-rwxr--r-- 1 98059 @4000000000025cb629c311ac.s
-rwxr--r-- 1 98061 @40000000000260f7309c7fb4.s
-rwxr--r-- 1 98041 @40000000000265233a6cc0b6.s
-rwxr--r-- 1 98019 @400000000002695d10c06929.s
-rwxr--r-- 1 98064 @4000000000026d84189559e0.s
-rwxr--r-- 1 98055 @40000000000271ce1e031d91.s
-rwxr--r-- 1 98054 @400000000002760229733626.s
-rwxr--r-- 1 98104 @4000000000027a2e3b6f4e12.s
-rwxr--r-- 1 98023 @4000000000027e6f0ed24a6c.s
-rw-r--r-- 1 42374 current
# tail -2 /run/log/current
@40000000000284f130747343 wan.link.pppoe Connect: ppp0 <--> /dev/pts/0
@40000000000284f230acc669 wan.link.pppoe sent [LCP ConfReq id=0x1 <asyncmap 0x0> <magic 0x667a9594> <pcomp> <accomp>]
# tail -2 /run/log/current | s6-tai64nlocal
1970-01-02 21:51:45.828598156 wan.link.pppoe sent [LCP ConfReq id=0x1 <asyncmap 0x0> <magic 0x667a9594> <pcomp> <accom
p>]
1970-01-02 21:51:48.832588765 wan.link.pppoe sent [LCP ConfReq id=0x1 <asyncmap 0x0> <magic 0x667a9594> <pcomp> <accom
p>]
Log persistence
---------------
Logs written to :file:`/run/log/` will not survive a reboot or crash,
as it is an ephemeral filesystem.
On supported hardware you can enable logging to `pstore
<https://www.kernel.org/doc/Documentation/ABI/testing/pstore>`_ which
means the most recent log messages will be preserved on reboot. Set
the config option ``logging.persistent.enable = true`` to log messages
to :file:`/dev/pmsg0` as well as to the regular log. This is a
circular buffer, so when it fills up newer messages will overwrite the
oldest messages.
Logs found in pstore after a reboot will be moved at startup to
:file:`/run/log/previous-boot`
Updating an installed system
****************************
If your system has a writable root filesystem (JFFS2, btrfs etc -
anything but squashfs), we have mechanisms for in-places updates
analogous to :command:`nixos-rebuild`, but the operation is a bit
different because it expects to run on a build machine and then copy
to the host device using :command:`ssh`.
To use this, build the ``outputs.updater``
target and then run the :command:`update.sh` script it generates.
.. code-block:: console
nix-build -I liminix-config=./my-configuration.nix \
--arg device "import ./devices/mydevice" \
-A outputs.updater
./result/bin/update.sh root@the-device
The update script uses min-copy-closure to copy new or changed
packages to the device, then (perhaps) reboots it. The reboot
behaviour can be affected by flags:
* `--no-reboot` will cause it not to reboot at all, if you would
rather do that yourself. Note that none of the newly-installed or
updated services will be running until you do.
* `--fast` causes it tn not do a full reboot, but instead to restart
only the services that have been changed. This will restart all of
the services that have updated store paths (and anything that
depends on them), but will not affect services that haven't changed.
It doesn't delete old packages automatically: to do that run
:command:`min-collect-garbage`, which will delete any packages not in
the current system closure. Note that Liminix does not have the NixOS
concept of environments or generations, and there is no way back from
this except for building the previous configuration again.
Caveats
-------
* it needs there to be enough free space on the device for all the new
packages in addition to all the packages already on it - which may
be a problem if there is little flash storage or if a lot of things
have changed (e.g. a new version of nixpkgs).
* it may not be able to upgrade the kernel: this is device-dependent.
If your device boots from a kernel image on a raw MTD partition or
or UBI volume, update.sh is unable to alter the kernel partition.
If your device boots from a kernel inside the filesystem (e.g. using
bootloader.extlinux or bootloder.fit) then the kernel will be
upgraded along with the userland
Recovery/downgrades
-------------------
The :command:`update.sh` script also creates a timestamped symlink on
the device which points to the system configuration it installs. If
you install a configuration that doesn't work, you can revert to any
other installed configuration by
1) booting to some kind of rescue or recovery system (which may be
some vendor-provided rescue option, or your own recovery system
perhaps based on :file:`examples/recovery.nix`) and mounting
your Liminix filesystem on /mnt
2) picking another previously-installed configuration that _did_ work,
and switching back to it:
.. code-block:: console
# ls -ld /mnt/*configuration
lrwxrwxrwx 1 90 /mnt/20252102T182104.configuration -> nix/store/v1w0h4zw65ah4c2r0k7nyy125qrxhq78-system-configuration-aarch64-unknown-linux-musl
lrwxrwxrwx 1 90 /mnt/20251802T181822.configuration -> nix/store/wqjl9s9xljl2wg8257292zghws9ssidk-system-configuration-aarch64-unknown-linux-musl
# : 20251802T181822 is the working system, so reinstall it
# /mnt/20251802T181822.configuration/bin/install /mnt
# umount /mnt
# reboot
This will install the previous configuration's activation binary into
/bin, and copy its kernel and initramfs into /boot. Note that it
depends on the previous system not having been garbage-collected.
.. _levitate:
Adding packages
===============
If you simply wish to add a package without any change to services,
you can call :command:`min-copy-closure` directly to install
any package in Nixpkgs or in the Liminix overlay
.. code-block:: console
nix-build -I liminix-config=./my-configuration.nix \
--arg device "import ./devices/mydevice" -A pkgs.tcpdump
nix-shell -p min-copy-closure root@the-device result/
Note that this only copies the package and its dependencies to the
device: it doesn't update any profile to add it to ``$PATH``
.. _rebuilding the system:
Reinstalling on a running system
********************************
Liminix is initially installed from a monolithic
:file:`firmware.bin` - and unless you're running a writable
filesystem, the only way to update it is to build and install a whole
new :file:`firmware.bin`. However, you probably would prefer not to
have to remove it from its installation site, unplug it from the
network and stick serial cables in it all over again.
It is not (generally) safe to install a new firmware onto the flash
partitions that the active system is running on. To address this we
have :command:`levitate`, which a way for a running Liminix system to
"soft restart" into a ramdisk running only a limited set of services,
so that the main partitions can then be safely flashed.
Configuration
=============
Levitate *needs to be configured when you create the initial system*
to specify which services/packages/etc to run in maintenance
mode. Most likely you want to configure a network interface and an ssh
for example so that you can login to reflash it.
.. code-block:: nix
defaultProfile.packages = with pkgs; [
...
(levitate.override {
config = {
services = {
inherit (config.services) dhcpc sshd watchdog;
};
defaultProfile.packages = [ mtdutils ];
users.root = config.users.root;
};
})
];
Use
===
Connect (with ssh, probably) to the running Liminix system that you
wish to upgrade.
.. code-block:: console
bash$ ssh root@the-device
Run :command:`levitate`. This takes a little while (perhaps a few
tens of seconds) to execute, and copies all config required for
maintenance mode to :file:`/run/maintenance`.
.. code-block:: console
# levitate
Reboot into maintenance mode. You will be logged out
.. code-block:: console
# reboot
Connect to the device again - note that the ssh host key will have changed.
.. code-block:: console
# ssh -o UserKnownHostsFile=/dev/null root@the-device
Check we're in maintenance mode
.. code-block:: console
# cat /etc/banner
LADIES AND GENTLEMEN WE ARE FLOATING IN SPACE
Most services are disabled. The system is operating
with a ram-based root filesystem, making it safe to
overwrite the flash devices in order to perform
upgrades and maintenance.
Don't forget to reboot when you have finished.
Perform the upgrade, using flashcp. This is an example,
your device will differ
.. code-block:: console
# cat /proc/mtd
dev: size erasesize name
mtd0: 00030000 00010000 "u-boot"
mtd1: 00010000 00010000 "u-boot-env"
mtd2: 00010000 00010000 "factory"
mtd3: 00f80000 00010000 "firmware"
mtd4: 00220000 00010000 "kernel"
mtd5: 00d60000 00010000 "rootfs"
mtd6: 00010000 00010000 "art"
# flashcp -v firmware.bin mtd:firmware
All done
.. code-block:: console
# reboot

470
doc/configuration.adoc Normal file
View File

@ -0,0 +1,470 @@
== Configuration
There are many things you can specify in a configuration, but these are
the ones you most commonly need to change:
* which services (processes) to run
* what packages to install
* permitted users and groups
* Linux kernel configuration options
* Busybox applets
* filesystem layout
=== Modules
*Modules* are a means of abstraction which allow "bundling" of
configuration options related to a common purpose or theme. For example,
the `+dnsmasq+` module defines a template for a dnsmasq service, ensures
that the dnsmasq package is installed, and provides a dnsmasq user and
group for the service to run as. The `+ppp+` module defines a service
template and also enables various PPP-related kernel configuration.
Not all modules are included in the configuration by default, because
that would mean that the kernel (and the Busybox binary providing common
CLI tools) was compiled with many unnecessary bells and whistles and
therefore be bigger than needed. (This is not purely an academic concern
if your device has little flash storage). Therefore, specifying a
service is usually a two-step process. For example, to add an NTP
service you first add `+modules/ntp+` to your `+imports+` list, then you
create a service by calling `+config.system.service.ntp.build { .... }+`
with the appropriate service-dependent configuration parameters.
[source,nix]
----
let svc = config.system.service;
in {
# ...
imports = [
./modules/ntp
# ....
];
config.services.ntp = svc.ntp.build {
pools = { "pool.ntp.org" = ["iburst"]; };
makestep = { threshold = 1.0; limit = 3; };
};
----
Merely including the module won't define the service on its own: it only
creates the template in `+config.system.service.foo+` and you have to
create an actual service using the template. This is an intentional
choice to allow the creation of multiple differently-configured services
based on the same template - perhaps e.g. when you have multiple
networks (VPNs etc) in different trust domains, or you want to run two
SSH daemons on different ports. (For the background to this, please
refer to the `+architecture decision record <adr/module-system>+`)
[TIP]
====
Liminix modules should be quite familiar (but also different) if you
already know how to use NixOS modules. We use the NixOS module
infrastructure code, meaning that you should recognise the syntax, the
type system, the rules for combining configuration values from different
sources. We don't use the NixOS modules themselves, because the
underlying system is not similar enough for them to work.
====
[[configuration-services]]
=== Services
In Liminix a service is any kind of long-running task or process on the
system, that is managed (started, stopped, and monitored) by a service
supervisor. A typical SOHO router might have services to
* answer DHCP and DNS requests from the LAN
* provide a wireless access point
* connect using PPPoE or L2TP to an upstream network
* start/stop the firewall
* enable/disable IP packet forwarding
* mount filesystems
(Some of these might not be considered services using other definitions
of the term: for example, this L2TP process would be a "client" in the
client/server classification; and enabling packet forwarding doesn't
require any long-lived process - just a setting to be toggled. However,
there is value in being able to use the same abstractions for all the
things to manage them and specify their dependency relationships - so in
Liminix "everything is a service")
The service supervision system enables service health monitoring,
restart of unhealthy services, and failover to "backup" services when a
primary service fails or its dependencies are unavailable. The intention
is that you have a framework in which you can specify policy
requirements like "ethernet wan dhcp-client should be restarted if it
crashes, but if it can't start because the hardware link is down, then
4G ppp service should be started instead".
Any attribute in [.title-ref]#config.services# will become part of the
default set of services that s6-rc will try to bring up. Services are
usually started at boot time, but *controlled services* are those that
are required only in particular contexts. For example, a service to
mount a USB backup drive should run only when the drive is attached to
the system. Liminix currently implements three kinds of controlled
service:
* "uevent-rule" service controllers use sysfs/uevent to identify when
particular hardware devices are present, and start/stop a controlled
service appropriately.
* the "round-robin" service controller is used for service failover: it
allows you to specify a list of services and runs each of them in turn
until it exits, then runs the next.
* the "health-check" service wraps another service, and runs a "health
check" command at regular intervals. When the health check fails,
indicating that the wrapped service is not working, it is terminated and
allowed to restart.
==== Runtime secrets (external vault)
Secrets (such as wifi passphrases, PPP username/password, SSH keys, etc)
that you provide as literal values in `+configuration.nix+` are
processed into into config files and scripts at build time, and
eventually end up in various files in the (world-readable)
`+/nix/store+` before being baked into a flashable image. To change a
secret - whether due to a compromise, or just as part of to a routine
key rotation - you need to rebuild the configuration and potentially
reflash the affected devices.
To avoid this, you may instead use a "secrets service", which is a
mechanism for your device to fetch secrets from a source external to the
Nix store, and create at runtime the configuration files and scripts
that start the services which require them.
Not every possible parameter to every possible service is configurable
using a secrets service. Parameters which can be configured this way are
those with the type `+liminix.lib.types.replacable+`. At the time this
document was written, these include:
* ppp (pppoe and l2tp): `+username+`, `+password+`
* ssh: `+authorizedKeys+`
* hostapd: all parameters (most likely to be useful for
`+wpa_passphrase+`)
To use a runtime secret for any of these parameters:
* create a secrets service to specify the source of truth for secrets
* use the `+outputRef+` function in the service parameter to specify the
secrets service and path
For example, given you had an HTTPS server hosting a JSON file with the
structure
[source,json]
----
"ssh": {
"authorizedKeys": {
"root": [ "ssh-rsa ....", "ssh-rsa ....", ... ]
"guest": [ "ssh-rsa ....", "ssh-rsa ....", ... ]
}
}
----
you could use a `+configuration.nix+` fragment something like this to
make those keys visible to ssh:
[source,nix]
----
services.secrets = svc.secrets.outboard.build {
name = "secret-service";
url = "http://10.0.0.1/secrets.json";
username = "secrets";
password = "liminix";
interval = 30; # minutes
dependencies = [ config.services.lan ];
};
services.sshd = svc.ssh.build {
authorizedKeys = outputRef config.services.secrets "ssh/authorizedKeys";
};
----
There are presently two implementations of a secrets service:
===== Outboard secrets (HTTPS)
This service expects a URL to a JSON file containing all the secrets.
You may specify a username and password along with the URL, which are
used if the file is password-protected (HTTP Basic authentication). Note
that this is not a protection against a malicious local user: the
username and password are normal build-time parameters so will be
readable in the Nix store. This is a mitigation against the URL being
accidentally discovered due to e.g. a log file or error message on the
server leaking.
===== Tang secrets (encrypted local file)
Aternatively, secrets may be stored locally on the device, in a file
that has been encrypted using https://github.com/latchset/tang[Tang].
____
Tang is a server for binding data to network presence.
This sounds fancy, but the concept is simple. You have some data, but
you only want it to be available when the system containing the data is
on a certain, usually secure, network.
____
[source,nix]
----
services.secrets = svc.secrets.tang.build {
name = "secret-service";
path = "/run/mnt/usbstick/secrets.json.jwe";
interval = 30; # minutes
dependencies = [ config.services.mount-usbstick ];
};
----
The encryption uses the same scheme/algorithm as
https://github.com/latchset/clevis[Clevis] : you may use the
https://github.com/latchset/clevis?tab=readme-ov-file#pin-tang[Clevis
instructions] to encrypt the file on another host and then copy it to
your Liminix device, or you can use `+tangc encrypt+` to encrypt
directly on the device. (That latter approach may pose a chicken/egg
problem if the device needs secrets to boot up and run the services you
are relying on in order to login).
==== Writing services
For the most part, for common use cases, hopefully the services you need
will be defined by modules and you will only have to pass the right
parameters to `+build+`.
Should you need to create a custom service of your own devising, use the
[.title-ref]#oneshot# or [.title-ref]#longrun# functions:
* a "longrun" service is the "normal" service concept: it has a `+run+`
action which describes the process to start, and it watches that process
to restart it if it exits. The process should not attempt to daemonize
or "background" itself, otherwise s6-rc will think it died. Whatever it
prints to standard output/standard error will be logged.
[source,nix]
----
config.services.cowsayd = pkgs.liminix.services.longrun {
name = "cowsayd";
run = "${pkgs.cowsayd}/bin/cowsayd --port 3001 --breed hereford";
# don't start this until the lan interface is ready
dependencies = [ config.services.lan ];
}
----
* a "oneshot" service doesn't have a process attached. It consists of
`+up+` and `+down+` actions which are bits of shell script that are run
at the appropriate points in the service lifecycle
[source,nix]
----
config.services.greenled = pkgs.liminix.services.oneshot {
name = "greenled";
up = ''
echo 17 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio17/direction
echo 0 > /sys/class/gpio/gpio17/value
'';
down = ''
echo 0 > /sys/class/gpio/gpio17/value
'';
}
----
Services may have dependencies: as you see above in the `+cowsayd+`
example, it depends on some service called `+config.services.lan+`,
meaning that it won't be started until that other service is up.
==== Service outputs
Outputs are a mechanism by which a service can provide data which may be
required by other services. For example:
* the DHCP client service can expect to receive nameserver address
information as one of the fields in the response from the DHCP server:
we provide that as an output which a dependent service for a stub name
resolver can use to configure its upstream servers.
* a service that creates a new network interface (e.g. ppp) will provide
the name of the interface (`+ppp0+`, or `+ppp1+` or `+ppp7+`) as an
output so that a dependent service can reference it to set up a route,
or to configure firewall rules.
A service `+myservice+` should write its outputs as files in
`+/run/services/outputs/myservice+`: you can look around this directory
on a running Liminix system to see how it's used currently. Usually we
use the `+in_outputs+` shell function in the `+up+` or `+run+`
attributes of the service:
[source,shell]
----
(in_outputs ${name}
for i in lease mask ip router siaddr dns serverid subnet opt53 interface ; do
(printenv $i || true) > $i
done)
----
The outputs are just files, so technically you can read them using
anything that can read a file. Liminix has two "preferred" mechanisms,
though:
===== One-off lookups
In any context that ends up being evaluated by the shell, use `+output+`
to print the value of an output
[source,nix]
----
services.defaultroute4 = svc.network.route.build {
via = "$(output ${services.wan} address)";
target = "default";
dependencies = [ services.wan ];
};
----
===== Continuous updates
The downside of using shell functions in downstream service startup
scripts is that they only run when the service starts up: if a service
output _changes_, the downstream service would have to be restarted to
notice the change. Sometimes this is OK but other times the downstream
has no other need to restart, if it can only get its new data.
For this case, there is the `+anoia.svc+` Fennel library, which allows
you to write a simple loop which is iterated over whenever a service's
outputs change. This code is from
`+modules/dhcp6c/acquire-wan-address.fnl+`
[source,fennel]
----
(fn update-addresses [wan-device addresses new-addresses exec]
;; run some appropriate "ip address [add|remove]" commands
)
(fn run []
(let [[state-directory wan-device] arg
dir (svc.open state-directory)]
(accumulate [addresses []
v (dir:events)]
(update-addresses wan-device addresses
(or (v:output "address") []) system))))
----
The `+output+` method seen here accepts a filename (relative to the
service's output directory), or a directory name. It returns the first
line of that file, or for directories it returns a table (Lua's
key/value datastructure, similar to a hash/dictionary) of the outputs in
that directory.
===== Output design considerations
For preference, outputs should be short and simple, and not require
downstream services to do complicated parsing in order to use them.
Shell commands in Liminix are run using the Busybox shell which doesn't
have the niceties of an advanced shell like Bash let alone those of a
real programming language.
Note also that the Lua `+svc+` library only reads the first line of each
output.
=== Module implementation
Modules in Liminix conventionally live in
`+modules/somename/default.nix+`. If you want or need to write your own,
you may wish to refer to the examples there in conjunction with reading
this section.
A module is a function that accepts `+{lib, pkgs, config, ... }+` and
returns an attrset with keys `+imports, options config+`.
* `+imports+` is a list of paths to the other modules required by this
one
* `+options+` is a nested set of option declarations
* `+config+` is a nested set of option definitions
The NixOS manual section
https://nixos.org/manual/nixos/stable/#sec-writing-modules[Writing NixOS
Modules] is a quite comprehensive reference to writing NixOS modules,
which is also mostly applicable to Liminix except that it doesn't cover
service templates.
==== Service templates
To expose a service template in a module, it needs the following:
* an option declaration for `+system.service.myservicename+` with the
type of `+liminix.lib.types.serviceDefn+`
[source,nix]
----
options = {
system.service.cowsay = mkOption {
type = liminix.lib.types.serviceDefn;
};
};
----
* an option definition for the same key, which specifies where to import
the service template from (often `+./service.nix+`) and the types of its
parameters.
[source,nix]
----
config.system.service.cowsay = config.system.callService ./service.nix {
address = mkOption {
type = types.str;
default = "0.0.0.0";
description = "Listen on specified address";
example = "127.0.0.1";
};
port = mkOption {
type = types.port;
default = 22;
description = "Listen on specified TCP port";
};
breed = mkOption {
type = types.str;
default = "British Friesian"
description = "Breed of the cow";
};
};
----
Then you need to provide the service template itself, probably in
`+./service.nix+`:
[source,nix]
----
{
# any nixpkgs package can be named here
liminix
, cowsayd
, serviceFns
, lib
}:
# these are the parameters declared in the callService invocation
{ address, port, breed} :
let
inherit (liminix.services) longrun;
inherit (lib.strings) escapeShellArg;
in longrun {
name = "cowsayd";
run = "${cowsayd}/bin/cowsayd --address ${address} --port ${builtins.toString port} --breed ${escapeShellArg breed}";
}
----
[TIP]
====
Not relevant to module-based services specifically, but a common gotcha
when specifiying services is forgetting to transform "rich" parameter
values into text when composing a command for the shell to execute. Note
here that the port number, an integer, is stringified with `+toString+`,
and the name of the breed, which may contain spaces, is escaped with
`+escapeShellArg+`
====
==== Types
All of the NixOS module types are available in Liminix. These
Liminix-specific types also exist in `+pkgs.liminix.lib.types+`:
* `+service+`: an s6-rc service
* `+interface+`: an s6-rc service which specifies a network interface
* `+serviceDefn+`: a service "template" definition
In the future it is likely that we will extend this to include other
useful types in the networking domain: for example; IP address, network
prefix or netmask, protocol family and others as we find them.

View File

@ -1,482 +0,0 @@
.. _configuration:
Configuration
#############
There are many things you can specify in a configuration, but these
are the ones you most commonly need to change:
* which services (processes) to run
* what packages to install
* permitted users and groups
* Linux kernel configuration options
* Busybox applets
* filesystem layout
Modules
*******
**Modules** are a means of abstraction which allow "bundling"
of configuration options related to a common purpose or theme. For
example, the ``dnsmasq`` module defines a template for a dnsmasq
service, ensures that the dnsmasq package is installed, and provides a
dnsmasq user and group for the service to run as. The ``ppp`` module
defines a service template and also enables various PPP-related kernel
configuration.
Not all modules are included in the configuration by default, because
that would mean that the kernel (and the Busybox binary providing
common CLI tools) was compiled with many unnecessary bells and whistles
and therefore be bigger than needed. (This is not purely an academic concern
if your device has little flash storage). Therefore, specifying a
service is usually a two-step process. For example, to add an NTP
service you first add :file:`modules/ntp` to your ``imports`` list,
then you create a service by calling
:code:`config.system.service.ntp.build { .... }` with the appropriate
service-dependent configuration parameters.
.. code-block:: nix
let svc = config.system.service;
in {
# ...
imports = [
./modules/ntp
# ....
];
config.services.ntp = svc.ntp.build {
pools = { "pool.ntp.org" = ["iburst"]; };
makestep = { threshold = 1.0; limit = 3; };
};
Merely including the module won't define the service on its own: it
only creates the template in ``config.system.service.foo`` and you
have to create an actual service using the template. This is an
intentional choice to allow the creation of multiple
differently-configured services based on the same template - perhaps
e.g. when you have multiple networks (VPNs etc) in different trust
domains, or you want to run two SSH daemons on different ports.
(For the background to this, please refer to the :doc:`architecture decision record <adr/module-system>`)
.. tip:: Liminix modules should be quite familiar (but also different)
if you already know how to use NixOS modules. We use the
NixOS module infrastructure code, meaning that you should
recognise the syntax, the type system, the rules for
combining configuration values from different sources. We
don't use the NixOS modules themselves, because the
underlying system is not similar enough for them to work.
.. _configuration-services:
Services
********
In Liminix a service is any kind of long-running task or process on
the system, that is managed (started, stopped, and monitored) by a
service supervisor. A typical SOHO router might have services to
* answer DHCP and DNS requests from the LAN
* provide a wireless access point
* connect using PPPoE or L2TP to an upstream network
* start/stop the firewall
* enable/disable IP packet forwarding
* mount filesystems
(Some of these might not be considered services using other
definitions of the term: for example, this L2TP process would be a
"client" in the client/server classification; and enabling packet
forwarding doesn't require any long-lived process - just a setting to
be toggled. However, there is value in being able to use the same
abstractions for all the things to manage them and specify their
dependency relationships - so in Liminix "everything is a service")
The service supervision system enables service health monitoring,
restart of unhealthy services, and failover to "backup" services when
a primary service fails or its dependencies are unavailable. The
intention is that you have a framework in which you can specify policy
requirements like "ethernet wan dhcp-client should be restarted if it
crashes, but if it can't start because the hardware link is down, then
4G ppp service should be started instead".
Any attribute in `config.services` will become part of the default set
of services that s6-rc will try to bring up. Services are usually
started at boot time, but **controlled services** are those that are
required only in particular contexts. For example, a service to mount
a USB backup drive should run only when the drive is attached to the
system. Liminix currently implements three kinds of controlled service:
* "uevent-rule" service controllers use sysfs/uevent to identify when
particular hardware devices are present, and start/stop a controlled
service appropriately.
* the "round-robin" service controller is used for service failover:
it allows you to specify a list of services and runs each of them
in turn until it exits, then runs the next.
* the "health-check" service wraps another service, and runs a "health
check" command at regular intervals. When the health check fails,
indicating that the wrapped service is not working, it is terminated
and allowed to restart.
Runtime secrets (external vault)
================================
Secrets (such as wifi passphrases, PPP username/password, SSH keys,
etc) that you provide as literal values in :file:`configuration.nix`
are processed into into config files and scripts at build time, and
eventually end up in various files in the (world-readable)
:file:`/nix/store` before being baked into a flashable image. To
change a secret - whether due to a compromise, or just as part of to a
routine key rotation - you need to rebuild the configuration and
potentially reflash the affected devices.
To avoid this, you may instead use a "secrets service", which is a
mechanism for your device to fetch secrets from a source external to
the Nix store, and create at runtime the configuration files and
scripts that start the services which require them.
Not every possible parameter to every possible service is configurable
using a secrets service. Parameters which can be configured this way
are those with the type ``liminix.lib.types.replacable``. At the time
this document was written, these include:
* ppp (pppoe and l2tp): ``username``, ``password``
* ssh: ``authorizedKeys``
* hostapd: all parameters (most likely to be useful for ``wpa_passphrase``)
To use a runtime secret for any of these parameters:
* create a secrets service to specify the source of truth for secrets
* use the :code:`outputRef` function in the service parameter to specify the secrets service and path
For example, given you had an HTTPS server hosting a JSON file with the structure
.. code-block:: json
"ssh": {
"authorizedKeys": {
"root": [ "ssh-rsa ....", "ssh-rsa ....", ... ]
"guest": [ "ssh-rsa ....", "ssh-rsa ....", ... ]
}
}
you could use a :file:`configuration.nix` fragment something like this
to make those keys visible to ssh:
.. code-block:: nix
services.secrets = svc.secrets.outboard.build {
name = "secret-service";
url = "http://10.0.0.1/secrets.json";
username = "secrets";
password = "liminix";
interval = 30; # minutes
dependencies = [ config.services.lan ];
};
services.sshd = svc.ssh.build {
authorizedKeys = outputRef config.services.secrets "ssh/authorizedKeys";
};
There are presently two implementations of a secrets service:
Outboard secrets (HTTPS)
------------------------
This service expects a URL to a JSON file containing all the secrets.
You may specify a username and password along with the URL, which are
used if the file is password-protected (HTTP Basic
authentication). Note that this is not a protection against a
malicious local user: the username and password are normal build-time
parameters so will be readable in the Nix store. This is a mitigation
against the URL being accidentally discovered due to e.g. a log file
or error message on the server leaking.
Tang secrets (encrypted local file)
-----------------------------------
Aternatively, secrets may be stored locally on the device, in a file
that has been encrypted using `Tang <https://github.com/latchset/tang>`_.
Tang is a server for binding data to network presence.
This sounds fancy, but the concept is simple. You have some data, but you only want it to be available when the system containing the data is on a certain, usually secure, network.
.. code-block:: nix
services.secrets = svc.secrets.tang.build {
name = "secret-service";
path = "/run/mnt/usbstick/secrets.json.jwe";
interval = 30; # minutes
dependencies = [ config.services.mount-usbstick ];
};
The encryption uses the
same scheme/algorithm as `Clevis <https://github.com/latchset/clevis>`_ : you may use the `Clevis instructions <https://github.com/latchset/clevis?tab=readme-ov-file#pin-tang>`_ to
encrypt the file on another host and then copy it to your Liminix
device, or you can use :command:`tangc encrypt` to encrypt directly on
the device. (That latter approach may pose a chicken/egg problem if
the device needs secrets to boot up and run the services you are
relying on in order to login).
Writing services
================
For the most part, for common use cases, hopefully the services you
need will be defined by modules and you will only have to pass the
right parameters to ``build``.
Should you need to create a custom service of your own devising, use
the `oneshot` or `longrun` functions:
* a "longrun" service is the "normal" service concept: it has a
``run`` action which describes the process to start, and it watches
that process to restart it if it exits. The process should not
attempt to daemonize or "background" itself, otherwise s6-rc will think
it died. Whatever it prints to standard output/standard error
will be logged.
.. code-block:: nix
config.services.cowsayd = pkgs.liminix.services.longrun {
name = "cowsayd";
run = "${pkgs.cowsayd}/bin/cowsayd --port 3001 --breed hereford";
# don't start this until the lan interface is ready
dependencies = [ config.services.lan ];
}
* a "oneshot" service doesn't have a process attached. It consists of
``up`` and ``down`` actions which are bits of shell script that
are run at the appropriate points in the service lifecycle
.. code-block:: nix
config.services.greenled = pkgs.liminix.services.oneshot {
name = "greenled";
up = ''
echo 17 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio17/direction
echo 0 > /sys/class/gpio/gpio17/value
'';
down = ''
echo 0 > /sys/class/gpio/gpio17/value
'';
}
Services may have dependencies: as you see above in the ``cowsayd``
example, it depends on some service called ``config.services.lan``,
meaning that it won't be started until that other service is up.
Service outputs
===============
Outputs are a mechanism by which a service can provide data which may
be required by other services. For example:
* the DHCP client service can expect to receive nameserver address
information as one of the fields in the response from the DHCP
server: we provide that as an output which a dependent service for a
stub name resolver can use to configure its upstream servers.
* a service that creates a new network interface (e.g. ppp) will
provide the name of the interface (:code:`ppp0`, or :code:`ppp1` or
:code:`ppp7`) as an output so that a dependent service can reference
it to set up a route, or to configure firewall rules.
A service :code:`myservice` should write its outputs as files in
:file:`/run/services/outputs/myservice`: you can look around this
directory on a running Liminix system to see how it's used currently.
Usually we use the :code:`in_outputs` shell function in the
:command:`up` or :command:`run` attributes of the service:
.. code-block:: shell
(in_outputs ${name}
for i in lease mask ip router siaddr dns serverid subnet opt53 interface ; do
(printenv $i || true) > $i
done)
The outputs are just files, so technically you can read them using
anything that can read a file. Liminix has two "preferred"
mechanisms, though:
One-off lookups
---------------
In any context that ends up being evaluated by the shell, use
:code:`output` to print the value of an output
.. code-block:: nix
services.defaultroute4 = svc.network.route.build {
via = "$(output ${services.wan} address)";
target = "default";
dependencies = [ services.wan ];
};
Continuous updates
------------------
The downside of using shell functions in downstream service startup
scripts is that they only run when the service starts up: if a service
output *changes*, the downstream service would have to be restarted to
notice the change. Sometimes this is OK but other times the downstream
has no other need to restart, if it can only get its new data.
For this case, there is the :code:`anoia.svc` Fennel library, which
allows you to write a simple loop which is iterated over whenever a
service's outputs change. This code is from
:file:`modules/dhcp6c/acquire-wan-address.fnl`
.. code-block:: fennel
(fn update-addresses [wan-device addresses new-addresses exec]
;; run some appropriate "ip address [add|remove]" commands
)
(fn run []
(let [[state-directory wan-device] arg
dir (svc.open state-directory)]
(accumulate [addresses []
v (dir:events)]
(update-addresses wan-device addresses
(or (v:output "address") []) system))))
The :code:`output` method seen here accepts a filename (relative
to the service's output directory), or a directory name. It
returns the first line of that file, or for directories it
returns a table (Lua's key/value datastructure, similar to
a hash/dictionary) of the outputs in that directory.
Output design considerations
----------------------------
For preference, outputs should be short and simple, and not require
downstream services to do complicated parsing in order to use them.
Shell commands in Liminix are run using the Busybox shell which
doesn't have the niceties of an advanced shell like Bash let alone
those of a real programming language.
Note also that the Lua :code:`svc` library only reads the first line
of each output.
Module implementation
*********************
Modules in Liminix conventionally live in
:file:`modules/somename/default.nix`. If you want or need to
write your own, you may wish to refer to the
examples there in conjunction with reading this section.
A module is a function that accepts ``{lib, pkgs, config, ... }`` and
returns an attrset with keys ``imports, options config``.
* ``imports`` is a list of paths to the other modules required by this one
* ``options`` is a nested set of option declarations
* ``config`` is a nested set of option definitions
The NixOS manual section `Writing NixOS Modules
<https://nixos.org/manual/nixos/stable/#sec-writing-modules>`_ is a
quite comprehensive reference to writing NixOS modules, which is also
mostly applicable to Liminix except that it doesn't cover
service templates.
Service templates
=================
To expose a service template in a module, it needs the following:
* an option declaration for ``system.service.myservicename`` with the
type of ``liminix.lib.types.serviceDefn``
.. code-block:: nix
options = {
system.service.cowsay = mkOption {
type = liminix.lib.types.serviceDefn;
};
};
* an option definition for the same key, which specifies where to
import the service template from (often :file:`./service.nix`)
and the types of its parameters.
.. code-block:: nix
config.system.service.cowsay = config.system.callService ./service.nix {
address = mkOption {
type = types.str;
default = "0.0.0.0";
description = "Listen on specified address";
example = "127.0.0.1";
};
port = mkOption {
type = types.port;
default = 22;
description = "Listen on specified TCP port";
};
breed = mkOption {
type = types.str;
default = "British Friesian"
description = "Breed of the cow";
};
};
Then you need to provide the service template itself, probably in
:file:`./service.nix`:
.. code-block:: nix
{
# any nixpkgs package can be named here
liminix
, cowsayd
, serviceFns
, lib
}:
# these are the parameters declared in the callService invocation
{ address, port, breed} :
let
inherit (liminix.services) longrun;
inherit (lib.strings) escapeShellArg;
in longrun {
name = "cowsayd";
run = "${cowsayd}/bin/cowsayd --address ${address} --port ${builtins.toString port} --breed ${escapeShellArg breed}";
}
.. tip::
Not relevant to module-based services specifically, but a common
gotcha when specifiying services is forgetting to transform "rich"
parameter values into text when composing a command for the shell
to execute. Note here that the port number, an integer, is
stringified with ``toString``, and the name of the breed,
which may contain spaces, is
escaped with ``escapeShellArg``
Types
=====
All of the NixOS module types are available in Liminix. These
Liminix-specific types also exist in ``pkgs.liminix.lib.types``:
* ``service``: an s6-rc service
* ``interface``: an s6-rc service which specifies a network
interface
* ``serviceDefn``: a service "template" definition
In the future it is likely that we will extend this to include other
useful types in the networking domain: for example; IP address,
network prefix or netmask, protocol family and others as we find them.

311
doc/development.adoc Normal file
View File

@ -0,0 +1,311 @@
== Development
As a developer working on Liminix, or implementing a service or module,
you probably want to test your changes more conveniently than by
building and flashing a new image every time. This section documents
various affordances for iteration and experiments.
In general, packages and tools that run on the "build" machine are
available in the `+buildEnv+` derivation and can most easily be added to
your environment by running `+nix-shell+`.
=== Emulated devices
Liminix has a `+qemu+` device, which generates images suitable for
running on your build machine using the free http://www.qemu.org[QEMU
machine emulator]. This is useful for developing userland without
needing to keep flashing or messing with U-Boot: it also enables testing
against emulated network peers using
https://wiki.qemu.org/Documentation/Networking#Socket[QEMU socket
networking], which may be preferable to letting Liminix loose on your
actual LAN. To build it,
[source,console]
----
nix-build -I liminix-config=path/to/your/configuration.nix --arg device "import ./devices/qemu" -A outputs.default
----
This creates a `+result/+` directory containing a `+vmlinux+` and a
`+rootfs+`, and also a shell script `+run.sh+` which invokes QEMU to run
that kernel with that filesystem. It connects the Liminix serial console
and the https://www.qemu.org/docs/master/system/monitor.html[QEMU
monitor] to stdin/stdout. Use ^P (not ^A) to switch to the monitor.
If you run with `+--background /path/to/some/directory+` as the first
parameter, it will fork into the background and open Unix sockets in
that directory for console and monitor. Use `+nix-shell --run
connect-vm+` to connect to either of these sockets, and ^O to
disconnect.
[[qemu-networking]]
==== Networking
VMs can network with each other using QEMU socket networking. We observe
these conventions, so that we can run multiple emulated instances and
have them wired up to each other in the right way:
* multicast 230.0.0.1:1234 : access (interconnect between router and
"isp")
* multicast 230.0.0.1:1235 : lan
* multicast 230.0.0.1:1236 : world (the internet)
Any VM started by a `+run.sh+` script is connected to "lan" and
"access", and the emulated border network gateway (see below) runs PPPoE
and is connected to "access" and "world".
===== Border Network Gateway
In pkgs/routeros there is a derivation to install and configure
https://mikrotik.com/software[Mikrotik RouterOS] as a PPPoE access
concentrator connected to the `+access+` and `+world+` networks, so that
Liminix PPPoE client support can be tested without actual hardware.
This is made available as the `+routeros+` command in `+buildEnv+`, so
you can do something like:
....
mkdir ros-sockets
nix-shell
nix-shell$ routeros ros-sockets
nix-shell$ connect-vm ./ros-sockets/console
....
to start it and connect to it. Note that by default it runs in the
background. It is connected to "access" and "world" virtual networks and
runs a PPPoE service on "access" - so a Liminix VM with a PPPOE client
can connect to it and thus reach the virtual internet. [ check, but
pretty sure this is not the actual internet ]
[.title-ref]#Liminix does not provide RouterOS licences and it is your
own responsibility if you use this to ensure you're compliant with the
terms of Mikrotik's licencing. It may be supplemented or replaced in
time with configurations for RP-PPPoE and/or Accel PPP.#
=== Hardware devices
==== TFTP
[[tftp server]]
How you get your image onto hardware will vary according to the device,
but is likely to involve taking it apart to add wires to serial console
pads/headers, then using U-Boot to fetch images over TFTP. The OpenWrt
documentation has a
https://openwrt.org/docs/techref/hardware/port.serial[good explanation]
of what you may expect to find on the device.
There is a rudimentary TFTP server bundled with the system which runs
from the command line, has an allowlist for client connections, and
follows symlinks, so you can have your device download images direct
from the `+./result+` directory without exposing `+/nix/store/+` to the
internet or mucking about copying files to `+/tftproot+`. If the
permitted device is to be given the IP address 192.168.8.251 you might
do something like this:
[source,console]
----
nix-shell --run "tufted -a 192.168.8.251 result"
----
Now add the device and server IP addresses to your configuration:
[source,nix]
----
boot.tftp = {
serverip = "192.168.8.111";
ipaddr = "192.168.8.251";
};
----
and then build the derivation for `+outputs.default+` or
`+outputs.mtdimage+` (for which it will be an alias on any device where
this is applicable). You should find it has created
* `+result/firmware.bin+` which is the file you are going to flash
* `+result/flash.scr+` which is a set of instructions to U-Boot to
download the image and write it to flash after erasing the appropriate
flash partition.
[NOTE]
====
TTL serial connections typically have no form of flow control and so
don't always like having massive chunks of text pasted into them - and
U-Boot may drop characters while it's busy. So don't necessarily expect
to copy-paste the whole of `+boot.scr+` into a terminal emulator and
have it work just like that. You may need to paste each line one at a
time, or even retype it.
====
For a faster edit-compile-test cycle, you can build a TFTP-bootable
image instead of flashing. In your device configuration add
[source,nix]
----
imports = [
./modules/tftpboot.nix
];
----
and then build `+outputs.tftpboot+`. This creates a file in `+result/+`
called `+boot.scr+`, which you can copy and paste into U-Boot to
transfer the kernel and filesystem over TFTP and boot the kernel from
RAM.
[[bng]]
==== Networking
You probably don't want to be testing a device that might serve DHCP,
DNS and routing protocols on the same LAN as you (or your colleagues,
employees, or family) are using for anything else, because it will
interfere. You also might want to test the device against an "upstream"
connection without having to unplug your regular home router from the
internet so you can borrow the cable/fibre/DSL.
`+bordervm+` is included for this purpose. You will need
* a Linux machine with a spare (PCI or USB) ethernet device which you
can dedicate to Liminix
* an L2TP service such as https://www.aa.net.uk/broadband/l2tp-service/
You need to "hide" the Ethernet device from the host - for PCI this
means configuring it for VFIO passthru; for USB you need to unload the
module(s) it uses. I have this segment in configuration.nix which you
may be able to adapt:
[source,nix]
----
boot = {
kernelParams = [ "intel_iommu=on" ];
kernelModules = [
"kvm-intel" "vfio_virqfd" "vfio_pci" "vfio_iommu_type1" "vfio"
];
postBootCommands = ''
# modprobe -i vfio-pci
# echo vfio-pci > /sys/bus/pci/devices/0000:01:00.0/driver_override
'';
blacklistedKernelModules = [
"r8153_ecm" "cdc_ether"
];
};
services.udev.extraRules = ''
SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="8153", OWNER="dan"
'';
----
Then you can execute `+run-border-vm+` in a `+buildEnv+` shell, which
starts up QEMU using the NixOS configuration in
`+bordervm-configuration.nix+`.
In this VM
* your Liminix checkout is mounted under `+/home/liminix/liminix+`
* TFTP is listening on the ethernet device and serving
`+/home/liminix/liminix+`. The server IP address is 10.0.0.1
* a PPPOE-L2TP relay is running on the same ethernet card. When the
connected Liminix device makes PPPoE requests, the relay spawns L2TPv2
Access Concentrator sessions to your specified L2TP LNS. Note that
authentication is expected at the PPP layer not the L2TP layer, so the
PAP/CHAP credentials provided by your L2TP service can be configured
into your test device - bordervm doesn't need to know about them.
To configure bordervm, you need a file called `+bordervm.conf.nix+`
which you can create by copying and appropriately editing
`+bordervm.conf-example.nix+`
[NOTE]
====
If you make changes to the bordervm configuration after executing
`+run-border-vm+`, you need to remove the `+border.qcow2+` disk image
file otherwise the changes won't get picked up.
====
=== Running tests
You can run all of the tests by evaluating `+ci.nix+`, which is the
input I use in Hydra.
[source,console]
----
nix-build -I liminix=`pwd` ci.nix -A pppoe # run one job
nix-build -I liminix=`pwd` ci.nix -A all # run all jobs
----
=== Troubleshooting
==== Diagnosing unexpectedly large images
Sometimes you can add a package and it causes the image size to balloon
because it has dependencies on other things you didn't know about. Build
the `+outputs.manifest+` attribute, which is a JSON representation of
the filesystem, and you can run `+nix-store --query+` on it.
[source,console]
----
nix-build -I liminix-config=path/to/your/configuration.nix \
--arg device "import ./devices/qemu" -A outputs.manifest \
-o manifest
nix-store -q --tree manifest
----
=== Contributing
Contributions are welcome, though in these early days there may be a bit
of back and forth involved before patches are merged: Please get in
touch somehow [.title-ref]#before# you invest a lot of time into a code
contribution I haven't asked for. Just so I know it's expected and
you're not wasting time doing something I won't accept or have already
started on.
==== Nix language style
This section describes some Nix language style points that we attempt to
adhere to in this repo.
* favour `+callPackage+` over raw `+import+` for calling derivations or
any function that may generate one - any code that might need `+pkgs+`
or parts of it.
* prefer `+let inherit (quark) up down strange charm+` over
`+with quark+`, in any context where the scope is more than a single
expression or there is more than one reference to `+up+`, `+down+` etc.
`+with pkgs; [ foo bar baz]+` is OK,
`+with lib; stdenv.mkDerivation { ... }+` is usually not.
* `+<liminix>+` is defined only when running tests, so don't refer to it
in "application" code
* the parameters to a derivation are sorted alphabetically, except for
`+lib+`, `+stdenv+` and maybe other non-package "special cases"
* indentation is whatever emacs nix-mode says it is.
* where a `+let+` form defines multiple names, put a newline after the
token `+let+`, and indent each name two characters
* to decide whether some code should be a package or a module? Packages
are self-contained - they live in `+/nix/store/eeeeeee-name+` and don't
directly change system behaviour by their presence or absense. modules
can add to `+/etc+` or `+/bin+` or other global state, create services,
all that side-effecty stuff. Generally it should be a package unless it
can't be.
==== Copyright
The Nix code in Liminix is MIT-licenced (same as Nixpkgs), but the code
it combines from other places (e.g. Linux, OpenWrt) may have a variety
of licences. I have no intention of asking for copyright assignment:
just like when submitting to the Linux kernel you retain the copyright
on the code you contribute.
==== Code of Conduct
Please govern yourself in Liminix project venues according to the
https://gti.telent.net/dan/liminix/src/commit/7bcf6b15c3fdddafeda13f65b3cd4a422dc52cd3/CODE-OF-CONDUCT.md[Code
of Conduct]
==== Where to send patches
Liminix' primary repo is https://gti.telent.net/dan/liminix but you
can't send code there directly because it doesn't have open
registrations.
* There's a https://github.com/telent/liminix[mirror on Github] for
convenience and visibility: you can open PRs against that
* or, you can send me your patch by email using
https://git-send-email.io/[git send-email]
* or in the future, some day, we will have federated Gitea using
ActivityPub.

View File

@ -1,340 +0,0 @@
Development
###########
As a developer working on Liminix, or implementing a service or
module, you probably want to test your changes more conveniently
than by building and flashing a new image every time. This section
documents various affordances for iteration and experiments.
In general, packages and tools that run on the "build" machine are
available in the ``buildEnv`` derivation and can most easily
be added to your environment by running :command:`nix-shell`.
Emulated devices
****************
Liminix has a ``qemu`` device, which generates images suitable for
running on your build machine using the free `QEMU machine emulator <http://www.qemu.org>`_.
This is useful for developing userland without needing to keep
flashing or messing with U-Boot: it also enables testing against
emulated network peers using `QEMU socket networking <https://wiki.qemu.org/Documentation/Networking#Socket>`_,
which may be preferable to letting Liminix loose on your actual LAN.
To build it,
.. code-block:: console
nix-build -I liminix-config=path/to/your/configuration.nix --arg device "import ./devices/qemu" -A outputs.default
This creates a :file:`result/` directory containing a :file:`vmlinux`
and a :file:`rootfs`, and also a shell script :file:`run.sh` which
invokes QEMU to run that kernel with that filesystem. It connects the Liminix
serial console and the `QEMU monitor <https://www.qemu.org/docs/master/system/monitor.html>`_ to stdin/stdout. Use ^P (not ^A) to switch to the monitor.
If you run with ``--background /path/to/some/directory`` as the first
parameter, it will fork into the background and open Unix sockets in
that directory for console and monitor. Use :command:`nix-shell --run
connect-vm` to connect to either of these sockets, and ^O to
disconnect.
.. _qemu-networking:
Networking
==========
VMs can network with each other using QEMU
socket networking. We observe these conventions, so that we can run
multiple emulated instances and have them wired up to each other in
the right way:
* multicast 230.0.0.1:1234 : access (interconnect between router and "isp")
* multicast 230.0.0.1:1235 : lan
* multicast 230.0.0.1:1236 : world (the internet)
Any VM started by a :command:`run.sh` script is connected to "lan" and
"access", and the emulated border network gateway (see below) runs
PPPoE and is connected to "access" and "world".
.. _border-network-gateway:
Border Network Gateway
----------------------
In pkgs/routeros there is a derivation to install and configure
`Mikrotik RouterOS <https://mikrotik.com/software>`_ as a PPPoE access
concentrator connected to the ``access`` and ``world`` networks, so that
Liminix PPPoE client support can be tested without actual hardware.
This is made available as the :command:`routeros` command in
``buildEnv``, so you can do something like::
mkdir ros-sockets
nix-shell
nix-shell$ routeros ros-sockets
nix-shell$ connect-vm ./ros-sockets/console
to start it and connect to it. Note that by default it runs in the
background. It is connected to "access" and "world" virtual networks
and runs a PPPoE service on "access" - so a Liminix VM with a
PPPOE client can connect to it and thus reach the virtual internet.
[ check, but pretty sure this is not the actual internet ]
`Liminix does not provide RouterOS licences and it is your own
responsibility if you use this to ensure you're compliant with the
terms of Mikrotik's licencing. It may be supplemented or replaced in
time with configurations for RP-PPPoE and/or Accel PPP.`
Hardware devices
****************
TFTP
====
.. _tftp server:
How you get your image onto hardware will vary according to the
device, but is likely to involve taking it apart to add wires to
serial console pads/headers, then using U-Boot to fetch images over
TFTP. The OpenWrt documentation has a `good explanation <https://openwrt.org/docs/techref/hardware/port.serial>`_ of what you may expect to find on
the device.
There is a rudimentary TFTP server bundled with the system which runs
from the command line, has an allowlist for client connections, and
follows symlinks, so you can have your device download images direct
from the :file:`./result` directory without exposing :file:`/nix/store/` to the
internet or mucking about copying files to :file:`/tftproot`. If the
permitted device is to be given the IP address 192.168.8.251 you might
do something like this:
.. code-block:: console
nix-shell --run "tufted -a 192.168.8.251 result"
Now add the device and server IP addresses to your configuration:
.. code-block:: nix
boot.tftp = {
serverip = "192.168.8.111";
ipaddr = "192.168.8.251";
};
and then build the derivation for ``outputs.default`` or
``outputs.mtdimage`` (for which it will be an alias on any device
where this is applicable). You should find it has created
* :file:`result/firmware.bin` which is the file you are going to flash
* :file:`result/flash.scr` which is a set of instructions to U-Boot to
download the image and write it to flash after erasing the appropriate
flash partition.
.. NOTE::
TTL serial connections typically have no form of flow control and
so don't always like having massive chunks of text pasted into
them - and U-Boot may drop characters while it's busy. So don't
necessarily expect to copy-paste the whole of :file:`boot.scr` into
a terminal emulator and have it work just like that. You may need
to paste each line one at a time, or even retype it.
For a faster edit-compile-test cycle, you can build a TFTP-bootable
image instead of flashing. In your device configuration add
.. code-block:: nix
imports = [
./modules/tftpboot.nix
];
and then build ``outputs.tftpboot``. This creates a file in
``result/`` called ``boot.scr``, which you can copy and paste into
U-Boot to transfer the kernel and filesystem over TFTP and boot the
kernel from RAM.
.. _bng:
Networking
==========
You probably don't want to be testing a device that might serve DHCP,
DNS and routing protocols on the same LAN as you (or your colleagues,
employees, or family) are using for anything else, because it will
interfere. You also might want to test the device against an
"upstream" connection without having to unplug your regular home
router from the internet so you can borrow the cable/fibre/DSL.
``bordervm`` is included for this purpose. You will need
* a Linux machine with a spare (PCI or USB) ethernet device which you can dedicate to Liminix
* an L2TP service such as https://www.aa.net.uk/broadband/l2tp-service/
You need to "hide" the Ethernet device from the host - for PCI this
means configuring it for VFIO passthru; for USB you need to unload the
module(s) it uses. I have this segment in configuration.nix which you
may be able to adapt:
.. code-block:: nix
boot = {
kernelParams = [ "intel_iommu=on" ];
kernelModules = [
"kvm-intel" "vfio_virqfd" "vfio_pci" "vfio_iommu_type1" "vfio"
];
postBootCommands = ''
# modprobe -i vfio-pci
# echo vfio-pci > /sys/bus/pci/devices/0000:01:00.0/driver_override
'';
blacklistedKernelModules = [
"r8153_ecm" "cdc_ether"
];
};
services.udev.extraRules = ''
SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="8153", OWNER="dan"
'';
Then
you can execute :command:`run-border-vm` in a ``buildEnv`` shell,
which starts up QEMU using the NixOS configuration in
:file:`bordervm-configuration.nix`.
In this VM
* your Liminix checkout is mounted under :file:`/home/liminix/liminix`
* TFTP is listening on the ethernet device and serving
:file:`/home/liminix/liminix`. The server IP address is 10.0.0.1
* a PPPOE-L2TP relay is running on the same ethernet card. When the
connected Liminix device makes PPPoE requests, the relay spawns
L2TPv2 Access Concentrator sessions to your specified L2TP LNS.
Note that authentication is expected at the PPP layer not the L2TP
layer, so the PAP/CHAP credentials provided by your L2TP service can
be configured into your test device - bordervm doesn't need to know
about them.
To configure bordervm, you need a file called :file:`bordervm.conf.nix`
which you can create by copying and appropriately editing :file:`bordervm.conf-example.nix`
.. note::
If you make changes to the bordervm configuration after executing
:command:`run-border-vm`, you need to remove the :file:`border.qcow2` disk
image file otherwise the changes won't get picked up.
Running tests
*************
You can run all of the tests by evaluating :file:`ci.nix`, which is the
input I use in Hydra.
.. code-block:: console
nix-build -I liminix=`pwd` ci.nix -A pppoe # run one job
nix-build -I liminix=`pwd` ci.nix -A all # run all jobs
Troubleshooting
***************
Diagnosing unexpectedly large images
====================================
Sometimes you can add a package and it causes the image size to balloon
because it has dependencies on other things you didn't know about. Build the
``outputs.manifest`` attribute, which is a JSON representation of the
filesystem, and you can run :command:`nix-store --query` on it.
.. code-block:: console
nix-build -I liminix-config=path/to/your/configuration.nix \
--arg device "import ./devices/qemu" -A outputs.manifest \
-o manifest
nix-store -q --tree manifest
Contributing
************
Contributions are welcome, though in these early days there may be a
bit of back and forth involved before patches are merged:
Please get in touch somehow `before` you invest a lot of time into a
code contribution I haven't asked for. Just so I know it's expected
and you're not wasting time doing something I won't accept or have
already started on.
Nix language style
==================
This section describes some Nix language style points that we
attempt to adhere to in this repo.
* favour ``callPackage`` over raw ``import`` for calling derivations
or any function that may generate one - any code that might need
``pkgs`` or parts of it.
* prefer ``let inherit (quark) up down strange charm`` over
``with quark``, in any context where the scope is more than a single
expression or there is more than one reference to ``up``, ``down``
etc. ``with pkgs; [ foo bar baz]`` is OK,
``with lib; stdenv.mkDerivation { ... }`` is usually not.
* ``<liminix>`` is defined only when running tests, so don't refer to it
in "application" code
* the parameters to a derivation are sorted alphabetically, except for
``lib``, ``stdenv`` and maybe other non-package "special cases"
* indentation is whatever emacs nix-mode says it is.
* where a ``let`` form defines multiple names, put a newline after the
token ``let``, and indent each name two characters
* to decide whether some code should be a package or a module?
Packages are self-contained - they live in ``/nix/store/eeeeeee-name``
and don't directly change system behaviour by their presence or
absense. modules can add to
``/etc`` or ``/bin`` or other global state, create services, all that
side-effecty stuff. Generally it should be a package unless it
can't be.
Copyright
=========
The Nix code in Liminix is MIT-licenced (same as Nixpkgs), but the
code it combines from other places (e.g. Linux, OpenWrt) may have a
variety of licences. I have no intention of asking for copyright
assignment: just like when submitting to the Linux kernel you retain
the copyright on the code you contribute.
Code of Conduct
===============
Please govern yourself in Liminix project venues according to the
`Code of Conduct <https://gti.telent.net/dan/liminix/src/commit/7bcf6b15c3fdddafeda13f65b3cd4a422dc52cd3/CODE-OF-CONDUCT.md>`_
Where to send patches
=====================
Liminix' primary repo is https://gti.telent.net/dan/liminix but you
can't send code there directly because it doesn't have open registrations.
* There's a `mirror on Github <https://github.com/telent/liminix>`_ for
convenience and visibility: you can open PRs against that
* or, you can send me your patch by email using `git send-email <https://git-send-email.io/>`_
* or in the future, some day, we will have federated Gitea using
ActivityPub.

10
doc/index.adoc Normal file
View File

@ -0,0 +1,10 @@
== Liminix
intro tutorial installation configuration admin development modules
hardware outputs
=== Indices and tables
* `+genindex+`
* `+modindex+`
* `+search+`

View File

@ -1,24 +0,0 @@
Liminix
#######
.. toctree::
:maxdepth: 3
:caption: Contents:
intro
tutorial
installation
configuration
admin
development
modules
hardware
outputs
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

203
doc/installation.adoc Normal file
View File

@ -0,0 +1,203 @@
== Installation
Hardware devices vary wildly in their affordances for installing new
operating systems, so it should be no surprise that the Liminix
installation procedure is hardware-dependent. This section contains
generic instructions, but please refer to the documentation for your
device to find whether and how well they apply.
=== Building a firmware image
Liminix uses the Nix language to provide congruent configuration
management. This means that to change anything about the way in which a
Liminix system works, you make that change in your `+configuration.nix+`
(or one of the other files it references), and rerun `+nix-build+` to
action the change. It is not possible (at least, without shenanigans) to
make changes by logging into the device and running imperative commands
whose effects may later be overridden: `+configuration.nix+` always
describes the entire system and can be used to recreate that system at
any time. You can usefully keep it under version control.
If you are familiar with NixOS, you will notice some similarities
between NixOS and Liminix configuration, and also some differences.
Sometimes the differences are due to the resource-constrained devices we
deploy onto, sometimes due to differences in the uses these devices are
put to.
For a more full description of how to configure Liminix, see
`+configuration+`. Assuming for the moment that you want a typical home
wireless gateway/router, the best way to get started is to copy
`+examples/rotuer.nix+` and edit it for your requirements.
[source,console]
----
$ cp examples/rotuer.nix configuration.nix
$ vi configuration.nix # other editors are available
$ # adjust this next command for your hardware device
$ nix-build -I liminix-config=./configuration.nix \
--arg device "import ./devices/gl-mt300a" -A outputs.default
----
Usually (not always, _please check the documentation for your device_)
this will leave you with a file `+result/firmware.bin+` which you now
need to flash to the device.
=== Flashing from the boot monitor (TFTP install)
If you are prepared to open the device and have a TTL serial adaptor of
some kind to connect it to, you can probably use U-Boot and a TFTP
server to download and flash the image.
This is quite hardware-specific and may even involve soldering - see the
documention for your device. However, it is in some ways the most
"reliable" option: if you can see what's happening (or not happening) in
early boot, the risk of "bricking" is substantially reduced and you have
options for recovering if you misstep or flash a bad image.
[[serial]]
==== U-Boot and serial shenanigans
Every device that we have so far encountered in Liminix uses
https://docs.u-boot.org/en/latest/[U-Boot&#44; the "Universal Boot
Loader"] so it's worth knowing a bit about it. "Universal" is in this
context a bit of a misnomer, though: encountering _mainline_ U-Boot is
very rare and often you'll find it is a fork from some version last
updated in 2008. Upgrading U-Boot is more or less complicated depending
on the device and is outside scope for Liminix.
To speak to U-Boot on your device you'll usually need a serial
connection to it. This typically involves opening the box, locating the
serial header pins (TX, RX and GND) and connecting a USB TTL converter
to them.
The Rolls Royce of USB/UART cables is the
https://cpc.farnell.com/ftdi/ttl-232r-rpi/cable-debug-ttl-232-usb-rpi/dp/SC12825?st=usb%20to%20uart%20cable[FTDI
cable], but there are cheaper alternatives based on the PL2303 and
CP2102 chipsets - or you could even get creative and use the
https://pinout.xyz/[UART GPIO pins] on a Raspberry Pi. Whatever you do,
make sure that the voltages are compatible: if your device is 3.3V (this
is typical but not universal), you don't want to be sending it 5v or
(even worse) 12v.
Run a terminal emulator such as Minicom on the computer at other end of
the link. 115200 8N1 is the typical speed.
[NOTE]
====
TTL serial connections often have no flow control and so don't always
like having massive chunks of text pasted into them - and U-Boot may
drop characters while it's busy. So don't do that.
If using Minicom, you may find it helps to bring up the "Termimal
settings" dialog (C^A T), then configure "Newline tx delay" to some
small but non-zero value.
====
When you turn the router on you should be greeted with some messages
from U-Boot, followed by the instruction to hit some key to stop
autoboot. Do this and you will get to the prompt. If you didn't see
anything, the strong likelihood is that TX and RX are the wrong way
around. If you see garbage, try a different speed.
Interesting commands to try first in U-Boot are `+help+` and
`+printenv+`.
You will also need to configure a TFTP server on a network that's
accessible to the device: how you do that will vary according to which
TFTP server you're using and so is out of scope for this document.
==== Building and installing the image
Follow the device-specific instructions for "TFTP install": usually, the
steps are
* build the [.title-ref]#outputs.mtdimage# output
* copy `+result/firmware.bin+` to your TFTP server
* copy/paste the commands in `+result/flash.scr+` one at a time into the
U-Boot command line
* reset the device
You should now see messages from U-Boot, then from the Linux kernel and
eventually a shell prompt.
[NOTE]
====
Before you reboot, check which networks the device is plugged into, and
disconnect as necessary. If you've just installed a DHCP server or
anything similar that responds to broadcasts, you may not want it to do
that on the network that you temporarily connected it to for installing
it.
====
=== Flashing from OpenWrt
[CAUTION]
====
Untested! A previous version of these instructions (without the -e flag)
led to bricking the device when flashing a jffs2 image. If you are
reading this message, nobody has yet reported on whether the new
instructions are any better.
====
If your device is running OpenWrt then it probably has the `+mtd+`
command installed. Build the [.title-ref]#outputs.mtdimage# output (as
you would for a TFTP install) and then transfer `+result/firmware.bin+`
onto the device using e.g. `+scp+`. Now flash as follows:
[source,console]
----
mtd -e -r write /tmp/firmware.bin firmware
----
The options to this command are for "erase before writing" and "reboot
after writing".
For more information, please see the
https://openwrt.org/docs/guide-user/installation/sysupgrade.cli[OpenWrt
manual] which may also contain (hardware-dependent) instructions on how
to flash an image using the vendor firmware - perhaps even from a web
interface.
=== Flashing from Liminix
If the device is already running Liminix and has been configured with
`+levitate+`, you can use that to safely flash your new image. Refer to
`+levitate+` for an explanation.
If the device is running Liminix but doesn't have `+levitate+` your
options are more limited. You may attempt to use `+flashcp+` but it
doesn't always work: as it copies the new image over the top of the
active root filesystem, surprise may ensue. Consider instead using a
serial connection: you may need one anyway after trying flashcp if it
corrupts the image.
==== flashcp (not generally recommended)
Connect to the device and locate the "firmware" partition, which you can
do with a combination of `+dmesg+` output and the contents of
`+/proc/mtd+`
[source,console]
----
<5>[ 0.469841] Creating 4 MTD partitions on "spi0.0":
<5>[ 0.474837] 0x000000000000-0x000000040000 : "u-boot"
<5>[ 0.480796] 0x000000040000-0x000000050000 : "u-boot-env"
<5>[ 0.487056] 0x000000050000-0x000000060000 : "art"
<5>[ 0.492753] 0x000000060000-0x000001000000 : "firmware"
# cat /proc/mtd
dev: size erasesize name
mtd0: 00040000 00001000 "u-boot"
mtd1: 00010000 00001000 "u-boot-env"
mtd2: 00010000 00001000 "art"
mtd3: 00fa0000 00001000 "firmware"
mtd4: 002a0000 00001000 "kernel"
mtd5: 00d00000 00001000 "rootfs"
----
Copy `+result/firmware.bin+` to the device and now run (in this example)
[source,console]
----
flashcp -v firmware.bin /dev/mtd3
----

View File

@ -1,211 +0,0 @@
Installation
############
Hardware devices vary wildly in their affordances for installing new
operating systems, so it should be no surprise that the Liminix
installation procedure is hardware-dependent. This section contains
generic instructions, but please refer to the documentation for your
device to find whether and how well they apply.
Building a firmware image
*************************
Liminix uses the Nix language to provide congruent configuration
management. This means that to change anything about the way in
which a Liminix system works, you make that change in
your :file:`configuration.nix` (or one of the other files it references),
and rerun :command:`nix-build` to action
the change. It is not possible (at least, without shenanigans) to make
changes by logging into the device and running imperative commands
whose effects may later be overridden: :file:`configuration.nix`
always describes the entire system and can be used to recreate that
system at any time. You can usefully keep it under version control.
If you are familiar with NixOS, you will notice some similarities
between NixOS and Liminix configuration, and also some
differences. Sometimes the differences are due to the
resource-constrained devices we deploy onto, sometimes due to
differences in the uses these devices are put to.
For a more full description of how to configure Liminix, see
:ref:`configuration`. Assuming for the moment that you want a typical
home wireless gateway/router, the best way to get started is to copy
:file:`examples/rotuer.nix` and edit it for your requirements.
.. code-block:: console
$ cp examples/rotuer.nix configuration.nix
$ vi configuration.nix # other editors are available
$ # adjust this next command for your hardware device
$ nix-build -I liminix-config=./configuration.nix \
--arg device "import ./devices/gl-mt300a" -A outputs.default
Usually (not always, *please check the documentation for your device*)
this will leave you with a file :file:`result/firmware.bin`
which you now need to flash to the device.
Flashing from the boot monitor (TFTP install)
*********************************************
If you are prepared to open the device and have a TTL serial adaptor
of some kind to connect it to, you can probably use U-Boot and a TFTP
server to download and flash the image.
This is quite hardware-specific and may even involve soldering - see
the documention for your device. However, it is in some ways the most
"reliable" option: if you can see what's happening (or not happening)
in early boot, the risk of "bricking" is substantially reduced and you
have options for recovering if you misstep or flash a bad image.
.. _serial:
U-Boot and serial shenanigans
=============================
Every device that we have so far encountered in Liminix uses `U-Boot,
the "Universal Boot Loader" <https://docs.u-boot.org/en/latest/>`_ so
it's worth knowing a bit about it. "Universal" is in this context a
bit of a misnomer, though: encountering *mainline* U-Boot is very rare
and often you'll find it is a fork from some version last updated
in 2008. Upgrading U-Boot is more or less complicated depending on the
device and is outside scope for Liminix.
To speak to U-Boot on your device you'll usually need a serial
connection to it. This typically involves opening the box, locating
the serial header pins (TX, RX and GND) and connecting a USB TTL
converter to them.
The Rolls Royce of USB/UART cables is the `FTDI cable
<https://cpc.farnell.com/ftdi/ttl-232r-rpi/cable-debug-ttl-232-usb-rpi/dp/SC12825?st=usb%20to%20uart%20cable>`_,
but there are cheaper alternatives based on the PL2303 and CP2102 chipsets - or you could even
get creative and use the `UART GPIO pins <https://pinout.xyz/>`_ on a Raspberry Pi. Whatever you do, make sure
that the voltages are compatible: if your device is 3.3V (this is
typical but not universal), you don't want to be sending it 5v or
(even worse) 12v.
Run a terminal emulator such as Minicom on the computer at other end
of the link. 115200 8N1 is the typical speed.
.. NOTE::
TTL serial connections often have no flow control and
so don't always like having massive chunks of text pasted into
them - and U-Boot may drop characters while it's busy. So don't
do that.
If using Minicom, you may find it helps to bring up the "Termimal
settings" dialog (C^A T), then configure "Newline tx delay" to
some small but non-zero value.
When you turn the router on you should be greeted with some messages
from U-Boot, followed by the instruction to hit some key to stop
autoboot. Do this and you will get to the prompt. If you didn't see
anything, the strong likelihood is that TX and RX are the wrong way
around. If you see garbage, try a different speed.
Interesting commands to try first in U-Boot are :command:`help` and
:command:`printenv`.
You will also need to configure a TFTP server on a network that's
accessible to the device: how you do that will vary according to which
TFTP server you're using and so is out of scope for this document.
Building and installing the image
=================================
Follow the device-specific instructions for "TFTP install": usually,
the steps are
* build the `outputs.mtdimage` output
* copy :file:`result/firmware.bin` to your TFTP server
* copy/paste the commands in :file:`result/flash.scr` one at a time into the U-Boot command line
* reset the device
You should now see messages from U-Boot, then from the Linux kernel
and eventually a shell prompt.
.. NOTE:: Before you reboot, check which networks the device is
plugged into, and disconnect as necessary. If you've just
installed a DHCP server or anything similar that responds to
broadcasts, you may not want it to do that on the network
that you temporarily connected it to for installing it.
Flashing from OpenWrt
*********************
.. CAUTION:: Untested! A previous version of these instructions
(without the -e flag) led to bricking the device
when flashing a jffs2 image. If you are reading
this message, nobody has yet reported on whether the
new instructions are any better.
If your device is running OpenWrt then it probably has the
:command:`mtd` command installed. Build the `outputs.mtdimage` output
(as you would for a TFTP install) and then transfer
:file:`result/firmware.bin` onto the device using e.g.
:command:`scp`. Now flash as follows:
.. code-block:: console
mtd -e -r write /tmp/firmware.bin firmware
The options to this command are for "erase before writing" and "reboot
after writing".
For more information, please see the `OpenWrt manual <https://openwrt.org/docs/guide-user/installation/sysupgrade.cli>`_ which may also contain (hardware-dependent) instructions on how to flash an image using the vendor firmware - perhaps even from a web interface.
Flashing from Liminix
*********************
If the device is already running Liminix and has been configured with
:command:`levitate`, you can use that to safely flash your new image.
Refer to :ref:`levitate` for an explanation.
If the device is running Liminix but doesn't have :command:`levitate`
your options are more limited. You may attempt to use
:command:`flashcp` but it doesn't always work: as it copies the new
image over the top of the active root filesystem, surprise may ensue.
Consider instead using a serial connection: you may need one anyway
after trying flashcp if it corrupts the image.
flashcp (not generally recommended)
===================================
Connect to the device and locate the "firmware" partition, which you
can do with a combination of :command:`dmesg` output and the contents
of :file:`/proc/mtd`
.. code-block:: console
<5>[ 0.469841] Creating 4 MTD partitions on "spi0.0":
<5>[ 0.474837] 0x000000000000-0x000000040000 : "u-boot"
<5>[ 0.480796] 0x000000040000-0x000000050000 : "u-boot-env"
<5>[ 0.487056] 0x000000050000-0x000000060000 : "art"
<5>[ 0.492753] 0x000000060000-0x000001000000 : "firmware"
# cat /proc/mtd
dev: size erasesize name
mtd0: 00040000 00001000 "u-boot"
mtd1: 00010000 00001000 "u-boot-env"
mtd2: 00010000 00001000 "art"
mtd3: 00fa0000 00001000 "firmware"
mtd4: 002a0000 00001000 "kernel"
mtd5: 00d00000 00001000 "rootfs"
Copy :file:`result/firmware.bin` to the device and now run (in this
example)
.. code-block:: console
flashcp -v firmware.bin /dev/mtd3

14
doc/intro.adoc Normal file
View File

@ -0,0 +1,14 @@
== Introduction
Liminix is a Nix-based collection of software tailored for domestic wifi
router or IoT device devices, of the kind that OpenWrt or DD-WRT or
Gargoyle or Tomato run on.
This is not NixOS-on-your-router: it's aimed at devices that are
underpowered for the full NixOS experience. It uses busybox tools, musl
instead of GNU libc, and s6-rc instead of systemd.
The Liminix name comes from Liminis, in Latin the genitive declension of
"limen", or "of the threshold". Your router stands at the threshold of
your (online) home and everything you send to/receive from the outside
word goes across it.

View File

@ -1,15 +0,0 @@
Introduction
############
Liminix is a Nix-based collection of software tailored for domestic
wifi router or IoT device devices, of the kind that OpenWrt or DD-WRT
or Gargoyle or Tomato run on.
This is not NixOS-on-your-router: it's aimed at devices that are
underpowered for the full NixOS experience. It uses busybox tools,
musl instead of GNU libc, and s6-rc instead of systemd.
The Liminix name comes from Liminis, in Latin the genitive declension
of "limen", or "of the threshold". Your router stands at the threshold
of your (online) home and everything you send to/receive from the
outside word goes across it.

1
doc/modules.adoc Normal file
View File

@ -0,0 +1 @@
== Module options

View File

@ -1,4 +0,0 @@
Module options
##############
.. include:: modules-generated.inc.rst

View File

@ -1,13 +1,10 @@
Outputs == Outputs
#######
Liminix *outputs* are artefacts that can be installed somehow on a Liminix _outputs_ are artefacts that can be installed somehow on a
target device, or "installers" which run on the target device to target device, or "installers" which run on the target device to perform
perform the installation. the installation.
There are different outputs because different target devices need There are different outputs because different target devices need
different artefacts, or have different ways to get that artefact different artefacts, or have different ways to get that artefact
installed. The options available for a particular device are described in installed. The options available for a particular device are described
the section for that device. in the section for that device.
.. include:: outputs-generated.inc.rst

322
doc/tutorial.adoc Normal file
View File

@ -0,0 +1,322 @@
== Tutorial
Liminix is very configurable, which can make it initially quite daunting
- especially if you're learning Nix or Linux or networking concepts at
the same time. In this section we build some "worked example" Liminix
images to introduce the concepts. If you follow the examples exactly,
they should work. If you change things as you go along, they may work
differently or not at all, but the experience should be educational
either way.
=== Requirements
You will need a reasonably powerful computer running Nix. Target devices
for Liminix are unlikely to have the CPU power and disk space to be able
to build it in situ, so the build process is based around
"cross-compilation" from another computer. The build machine can be any
reasonably powerful desktop/laptop/server PC running NixOS. Standalone
Nixpkgs installations on other Linux distributions - or on MacOS, or
even in a Docker container - also ought to work but are untested.
=== Running in Qemu
You can try out Liminix without even having a router to play with. Clone
the Liminix git repository and change into its directory
[source,console]
----
git clone https://gti.telent.net/dan/liminix
cd liminix
----
Now build Liminix
[source,console]
----
nix-build -I liminix-config=./examples/hello-from-qemu.nix \
--arg device "import ./devices/qemu" -A outputs.default
----
In this command `+liminix-config+` points to the desired software
configuration (e.g. services, users, filesystem, secrets) and `+device+`
describes the hardware (or emulated hardware) to run it on.
`+outputs.default+` tells Liminix that we want the default image output
for flashing to the device: for the Qemu "hardware" it's an alias for
`+outputs.vmbuild+`, which creates a directory containing a root
filesystem image and a kernel.
[TIP]
====
The first time you run this it may take several hours, because it builds
all of the dependencies including a full MIPS gcc and library toolchain.
Once those intermediate build products are in the nix store, subsequent
builds will be much faster - practically instant, if nothing has
changed.
====
Now you can try it:
[source,console]
----
./result/run.sh
----
This starts the Qemu emulator with a bunch of useful options, to run the
Liminix configuration you just built. It connects the emulated device's
serial console and the
https://www.qemu.org/docs/master/system/monitor.html[QEMU monitor] to
stdin/stdout.
You should now see Linux boot messages and after a few seconds be
presented with a root shell prompt. You can run commands to look at the
filesystem, see what processes are running, view log messages (in
:file:/run/log/current), etc. To kill the emulator, press ^P (Control P)
then c to enter the "QEMU Monitor", then type `+quit+` at the `+(qemu)+`
prompt.
To see that it's running network services we need to connect to its
emulated network. Start the machine again, if you had stopped it, and
open up a second terminal on your build machine. We're going to run
another virtual machine attached to the virtual network, which will
request an IP address from our Liminix system and give you a shell you
can run ssh from.
We use https://www.system-rescue.org/[System Rescue] in tty mode (no
graphical output) for this example, but if you have some other favourite
Linux Live CD ISO - or, for that matter, any other OS image that QEMU
can boot - adjust the command to suit.
Download the System Rescue ISO:
[source,console]
----
curl https://fastly-cdn.system-rescue.org/releases/10.01/systemrescue-10.01-amd64.iso -O
----
and run it
[source,console]
----
nix-shell -p qemu --run " \
qemu-system-x86_64 \
-echr 16 \
-m 1024 \
-cdrom systemrescue-10.01-amd64.iso \
-netdev socket,mcast=230.0.0.1:1235,localaddr=127.0.0.1,id=lan \
-device virtio-net,disable-legacy=on,disable-modern=off,netdev=lan,mac=ba:ad:3d:ea:21:01 \
-display none -serial mon:stdio"
----
System Rescue displays a boot menu at which you should select the
"serial console" option, then after a few moments it boots to a root
prompt. You can now try things out:
* run `+ip a+` and see that it's been allocated an IP address in the
range 10.3.0.0/16.
* run `+ping 10.3.0.1+` to see that the Liminix VM responds
* run `+ssh root@10.3.0.1+` to try logging into it.
Congratulations! You have installed your first Liminix system - albeit
it has no practical use and it's not even real. The next step is to try
running it on hardware.
=== Installing on hardware
For the next example, we're going to install onto an actual hardware
device. These steps have been tested using a GL.iNet GL-MT300A, which
has been chosen for the purpose because it's cheap and easy to unbrick
if necessary.
[WARNING]
====
There is always a risk of rendering your device unbootable by flashing
it with an image that doesn't work. The GL-MT300A has a builtin
"debrick" procedure in the boot monitor and is also comparatively simple
to attach serial cables to (soldering not required), so it is lower-risk
than some devices. Using some other Liminix-supported MIPS hardware
device also _ought_ to work here, but you accept the slightly greater
bricking risk if it doesn't.
====
See `+hardware+` for device support status.
You may want to read and inwardly digest the Develoment Manual section
`+serial+` when you start working with Liminix on real hardware. You
won't _need_ serial access for this example, assuming it works, but it
allows you to see the boot monitor and kernel messages, and to login
directly to the device if for some reason it doesn't bring its network
up.
Now we can build Liminix. Although we could use the same example
configuration as we did for Qemu, you might not want to plug a DHCP
server into your working LAN because it will compete with the real DHCP
service. So we're going to use a different configuration with a DHCP
client: this is `+examples/hello-from-mt300.nix+`
It's instructive to compare the two configurations:
[source,console]
----
diff -u examples/hello-from-qemu.nix examples/hello-from-mt300.nix
----
You'll see a new `+boot.tftp+` stanza which you can ignore,
`+services.dns+` has been removed, and the static IP address allocation
has been replaced by a `+dhcp.client+` service.
[source,console]
----
nix-build -I liminix-config=./examples/hello-from-mt300.nix \
--arg device "import ./devices/gl-mt300a" -A outputs.default
----
[TIP]
====
The first time you run this it may take several hours. Again? Yes, even
if you ran the previous example. Qemu is set up as a big-endian system
whereas the MediaTek SoC on this device is little-endian - so it
requires building all of the dependencies including an entirely
different MIPS gcc and library toolchain to the other one.
====
This time in `+result/+` you will see a bunch of files. Most of them you
can ignore for the moment, but `+result/firmware.bin+` is the firmware
image you can flash.
==== Flashing
Again, there are a number of different ways you could do this: using
TFTP with a serial cable, through the stock firmware's web UI, or using
the https://docs.gl-inet.com/router/en/3/tutorials/debrick/[vendor's
"debrick" process]. The last of these options has a lot to recommend it
for a first attempt:
* it works no matter what firmware is currently installed
* it doesn't require plugging a router into the same network as your
build system and potentially messing up your actual upstream
* no need to open the device and add cables
You can read detailed instructions on the vendor site, but the short
version is:
[arabic]
. turn the device off
. connect it by ethernet cable to a computer
. configure the computer to have static ip address 192.168.1.10
. while holding down the Reset button, turn the device on
. after about five seconds you can release the Reset button
. visit http://192.168.1.1/ using a web browser on the connected
computer
. click on "Browse" and choose `+result/firmware.bin+`
. click on "Update firmware"
. wait a minute or so while it updates.
There's no feedback from the web interface when the flashing is
finished, but what should happen is that the router reboots and starts
running Liminix. Now you need to figure out what address it got from
DHCP - e.g. by checking the DHCP server logs, or maybe by pinging
`+hello.lan+` or something. Once you've found it on the network you can
ping it and ssh to it just like you did the Qemu example, but this time
for real.
[WARNING]
====
Do not leave the default root password in place on any device exposed to
the internet! Although it has no writable storage and no default route,
a motivated attacker with some imagination could probably still do
something awful using it.
====
Congratulations Part II! You have installed your first Liminix system on
actual hardware - albeit that it _still_ has no practical use.
Exercise for the reader: change the default password by editing
`+examples/hello-from-mt300.nix+`, and then create and upload a new
image that has it set to something less hopeless.
=== Routing
The third example `+examples/demo.nix+` is a fully-functional home "WiFi
router" - although you will have to edit it a bit before it will
actually work for you. Copy `+examples/demo.nix+` to `+my-router.nix+`
(or other name of your choice) and open it in your favourite text
editor. Everywhere that the text `+EDIT+` appears is either a place you
probably want to change or a place you almost certainly need to change.
There's a lot going on in this configuration:
* it provides a wireless access point using the `+hostapd+` service: in
this stanza you can change the ssid, the channel, the passphrase etc.
* the wireless lan and wired lan are bridged together with the
`+bridge+` service, so that your wired and wireless clients appear to be
on the same network.
[TIP]
====
If you were using a hardware device that provides both 2.4GHz and 5GHz
wifi, you'd probably find that it has two wireless devices (often called
wlan0 and wlan1). In Liminix we handle this by running two `+hostapd+`
services, and adding both of them to the network bridge along with the
wired lan. (You can see an example in `+examples/rotuer.nix+`)
====
* we use the combination DNS and DHCP daemon provided by the `+dnsmasq+`
service, which you can configure
* the upstream network is "PPP over Ethernet", provided by the `+pppoe+`
service. Assuming that your ISP uses this standard, they will have
provided you with a PPP username and password (sometimes this will be
listed as "PAP" or "CHAP") which you can edit into the configuration
* this example supports the
newfootnote:[https://datatracker.ietf.org/doc/html/rfc1883[RFC1883
Internet Protocol&#44; Version 6] was published in 1995, so only "new"
when Bill Clinton was US President] Internet Protocol v6 as well as
traditional IPv4. Configuring IPv6 seems to vary from one ISP to the
next: this example expects them to be providing IP address allocation
and "prefix delegation" using DHCP6.
Build it using the same method as the previous example
[source,console]
----
nix-build -I liminix-config=./my-router.nix \
--arg device "import ./devices/gl-mt300a" -A outputs.default
----
and then you can flash it to the device.
==== Bonus: in-place updates
This configuration uses a writable filesystem (see the line
`+rootfsType = "jffs2"+`), which means that once you've flashed it for
the first time, you can make further updates over SSH onto the running
router. To try this, make a small change (I'd suggest changing the
hostname) and then run
[source,console]
----
nix-build -I liminix-config=./my-router.nix \
--arg device "import ./devices/gl-ar750" \
-A outputs.systemConfiguration && \
result/install.sh root@address-of-the-device
----
(This requires the device to be network-accessible from your build
machine, which for a test/demo system might involve a second network
device in your build system - USB ethernet adapters are cheap - or a bit
of messing around unplugging cables.)
For more information about in-place-updates, see the manual section
`+Rebuilding the system+`.
=== Final thoughts
* These are demonstration configs for pedagogical purposes. If you'd
like to see some more realistic uses of Liminix,
`+examples/rotuer,arhcive,extneder.nix+` are based on some actual real
hosts in my home network.
* The technique used here for flashing was chosen mostly because it
doesn't need much infrastructure/tooling, but it is a bit of a faff
(requires physical access, vendor specific). There are slicker ways to
do it that need a bit more setup - we'll talk about that later as well.
*Footnotes*

View File

@ -1,327 +0,0 @@
Tutorial
########
Liminix is very configurable, which can make it initially quite
daunting - especially if you're learning Nix or Linux or networking
concepts at the same time. In this section we build some "worked
example" Liminix images to introduce the concepts. If you follow the
examples exactly, they should work. If you change things as you go
along, they may work differently or not at all, but the experience
should be educational either way.
Requirements
************
You will need a reasonably powerful computer running Nix. Target
devices for Liminix are unlikely to have the CPU power and disk space
to be able to build it in situ, so the build process is based around
"cross-compilation" from another computer. The build machine can be
any reasonably powerful desktop/laptop/server PC running NixOS.
Standalone Nixpkgs installations on other Linux distributions - or on
MacOS, or even in a Docker container - also ought to work but are
untested.
Running in Qemu
***************
You can try out Liminix without even having a router to play with.
Clone the Liminix git repository and change into its directory
.. code-block:: console
git clone https://gti.telent.net/dan/liminix
cd liminix
Now build Liminix
.. code-block:: console
nix-build -I liminix-config=./examples/hello-from-qemu.nix \
--arg device "import ./devices/qemu" -A outputs.default
In this command ``liminix-config`` points to the desired software
configuration (e.g. services, users, filesystem, secrets) and
``device`` describes the hardware (or emulated hardware) to run it on.
``outputs.default`` tells Liminix that we want the default image
output for flashing to the device: for the Qemu "hardware" it's an
alias for ``outputs.vmbuild``, which creates a directory containing a
root filesystem image and a kernel.
.. tip:: The first time you run this it may take several hours,
because it builds all of the dependencies including a full
MIPS gcc and library toolchain. Once those intermediate build
products are in the nix store, subsequent builds will be much
faster - practically instant, if nothing has changed.
Now you can try it:
.. code-block:: console
./result/run.sh
This starts the Qemu emulator with a bunch of useful options, to run
the Liminix configuration you just built. It connects the emulated
device's serial console and the `QEMU monitor
<https://www.qemu.org/docs/master/system/monitor.html>`_ to
stdin/stdout.
You should now see Linux boot messages and after a few seconds be
presented with a root shell prompt. You can run commands to look at
the filesystem, see what processes are running, view log messages (in
:file:/run/log/current), etc. To kill the emulator, press ^P
(Control P) then c to enter the "QEMU Monitor", then type ``quit`` at
the ``(qemu)`` prompt.
To see that it's running network services we need to connect to its
emulated network. Start the machine again, if you had stopped it, and
open up a second terminal on your build machine. We're going to run
another virtual machine attached to the virtual network, which will
request an IP address from our Liminix system and give you a shell you
can run ssh from.
We use `System Rescue <https://www.system-rescue.org/>`_ in tty
mode (no graphical output) for this example, but if you have some
other favourite Linux Live CD ISO - or, for that matter, any other OS
image that QEMU can boot - adjust the command to suit.
Download the System Rescue ISO:
.. code-block:: console
curl https://fastly-cdn.system-rescue.org/releases/10.01/systemrescue-10.01-amd64.iso -O
and run it
.. code-block:: console
nix-shell -p qemu --run " \
qemu-system-x86_64 \
-echr 16 \
-m 1024 \
-cdrom systemrescue-10.01-amd64.iso \
-netdev socket,mcast=230.0.0.1:1235,localaddr=127.0.0.1,id=lan \
-device virtio-net,disable-legacy=on,disable-modern=off,netdev=lan,mac=ba:ad:3d:ea:21:01 \
-display none -serial mon:stdio"
System Rescue displays a boot menu at which you should select the
"serial console" option, then after a few moments it boots to a root
prompt. You can now try things out:
* run :command:`ip a` and see that it's been allocated an IP address in the range 10.3.0.0/16.
* run :command:`ping 10.3.0.1` to see that the Liminix VM responds
* run :command:`ssh root@10.3.0.1` to try logging into it.
Congratulations! You have installed your first Liminix system - albeit
it has no practical use and it's not even real. The next step is to try
running it on hardware.
Installing on hardware
**********************
For the next example, we're going to install onto an actual hardware
device. These steps have been tested using a GL.iNet GL-MT300A, which
has been chosen for the purpose because it's cheap and easy to
unbrick if necessary.
.. warning:: There is always a risk of rendering your device
unbootable by flashing it with an image that doesn't
work. The GL-MT300A has a builtin "debrick" procedure in
the boot monitor and is also comparatively simple to
attach serial cables to (soldering not required), so it
is lower-risk than some devices. Using some other
Liminix-supported MIPS hardware device also *ought* to
work here, but you accept the slightly greater bricking
risk if it doesn't.
See :doc:`hardware` for device support status.
You may want to read and inwardly digest the Develoment Manual section
:ref:`serial` when you start working with Liminix on real hardware. You
won't *need* serial access for this example, assuming it works, but it
allows you
to see the boot monitor and kernel messages, and to login directly to
the device if for some reason it doesn't bring its network up.
Now we can build Liminix. Although we could use the same example
configuration as we did for Qemu, you might not want to plug a DHCP
server into your working LAN because it will compete with the real
DHCP service. So we're going to use a different configuration with a
DHCP client: this is :file:`examples/hello-from-mt300.nix`
It's instructive to compare the two configurations:
.. code-block:: console
diff -u examples/hello-from-qemu.nix examples/hello-from-mt300.nix
You'll see a new ``boot.tftp`` stanza which you can ignore,
``services.dns`` has been removed, and the static IP address allocation
has been replaced by a ``dhcp.client`` service.
.. code-block:: console
nix-build -I liminix-config=./examples/hello-from-mt300.nix \
--arg device "import ./devices/gl-mt300a" -A outputs.default
.. tip:: The first time you run this it may take several hours.
Again? Yes, even if you ran the previous example. Qemu is
set up as a big-endian system whereas the MediaTek SoC
on this device is little-endian - so it requires building
all of the dependencies including an entirely different
MIPS gcc and library toolchain to the other one.
This time in :file:`result/` you will see a bunch of files. Most of
them you can ignore for the moment, but :file:`result/firmware.bin` is
the firmware image you can flash.
Flashing
========
Again, there are a number of different ways you could do this: using
TFTP with a serial cable, through the stock firmware's web UI, or
using the `vendor's "debrick" process
<https://docs.gl-inet.com/router/en/3/tutorials/debrick/>`_. The last
of these options has a lot to recommend it for a first attempt:
* it works no matter what firmware is currently installed
* it doesn't require plugging a router into the same network as your
build system and potentially messing up your actual upstream
* no need to open the device and add cables
You can read detailed instructions on the vendor site, but the short version is:
1. turn the device off
2. connect it by ethernet cable to a computer
3. configure the computer to have static ip address 192.168.1.10
4. while holding down the Reset button, turn the device on
5. after about five seconds you can release the Reset button
6. visit http://192.168.1.1/ using a web browser on the connected computer
7. click on "Browse" and choose :file:`result/firmware.bin`
8. click on "Update firmware"
9. wait a minute or so while it updates.
There's no feedback from the web interface when the flashing is
finished, but what should happen is that the router reboots and
starts running Liminix. Now you need to figure out what address it got
from DHCP - e.g. by checking the DHCP server logs, or maybe by pinging
``hello.lan`` or something. Once you've found it on the
network you can ping it and ssh to it just like you did the Qemu
example, but this time for real.
.. warning:: Do not leave the default root password in place on any
device exposed to the internet! Although it has no
writable storage and no default route, a motivated attacker
with some imagination could probably still do something
awful using it.
Congratulations Part II! You have installed your first Liminix system on actual hardware - albeit that it *still* has no practical use.
Exercise for the reader: change the default password by editing
:file:`examples/hello-from-mt300.nix`, and then create and upload a
new image that has it set to something less hopeless.
Routing
*******
The third example :file:`examples/demo.nix` is a fully-functional home
"WiFi router" - although you will have to edit it a bit before it will
actually work for you. Copy :file:`examples/demo.nix` to
:file:`my-router.nix` (or other name of your choice) and open it in
your favourite text editor. Everywhere that the text :code:`EDIT`
appears is either a place you probably want to change or a place you
almost certainly need to change.
There's a lot going on in this configuration:
* it provides a wireless access point using the :code:`hostapd`
service: in this stanza you can change the ssid, the channel,
the passphrase etc.
* the wireless lan and wired lan are bridged together with the
:code:`bridge` service, so that your wired and wireless clients appear
to be on the same network.
.. tip:: If you were using a hardware device that provides both 2.4GHz
and 5GHz wifi, you'd probably find that it has two wireless
devices (often called wlan0 and wlan1). In Liminix we handle
this by running two :code:`hostapd` services, and adding
both of them to the network bridge along with the wired lan.
(You can see an example in :file:`examples/rotuer.nix`)
* we use the combination DNS and DHCP daemon provided by the
:code:`dnsmasq` service, which you can configure
* the upstream network is "PPP over Ethernet", provided by the
:code:`pppoe` service. Assuming that your ISP uses this standard,
they will have provided you with a PPP username and password
(sometimes this will be listed as "PAP" or "CHAP") which you can edit
into the configuration
* this example supports the new [#ipv6]_ Internet Protocol v6
as well as traditional IPv4. Configuring IPv6 seems to
vary from one ISP to the next: this example expects them
to be providing IP address allocation and "prefix delegation"
using DHCP6.
Build it using the same method as the previous example
.. code-block:: console
nix-build -I liminix-config=./my-router.nix \
--arg device "import ./devices/gl-mt300a" -A outputs.default
and then you can flash it to the device.
Bonus: in-place updates
=======================
This configuration uses a writable filesystem (see the line
:code:`rootfsType = "jffs2"`), which means that once you've flashed it
for the first time, you can make further updates over SSH onto the
running router. To try this, make a small change (I'd suggest changing
the hostname) and then run
.. code-block:: console
nix-build -I liminix-config=./my-router.nix \
--arg device "import ./devices/gl-ar750" \
-A outputs.systemConfiguration && \
result/install.sh root@address-of-the-device
(This requires the device to be network-accessible from your build
machine, which for a test/demo system might involve a second network
device in your build system - USB ethernet adapters are cheap - or
a bit of messing around unplugging cables.)
For more information about in-place-updates, see the manual section :ref:`Rebuilding the system`.
Final thoughts
**************
* These are demonstration configs for pedagogical purposes. If you'd
like to see some more realistic uses of Liminix,
:file:`examples/rotuer,arhcive,extneder.nix` are based on some
actual real hosts in my home network.
* The technique used here for flashing was chosen mostly because it
doesn't need much infrastructure/tooling, but it is a bit of a faff
(requires physical access, vendor specific). There are slicker ways
to do it that need a bit more setup - we'll talk about that later as
well.
.. rubric:: Footnotes
.. [#ipv6] `RFC1883 Internet Protocol, Version 6 <https://datatracker.ietf.org/doc/html/rfc1883>`_ was published in 1995, so only "new" when Bill Clinton was US President