Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

260 changed files with 2599 additions and 14218 deletions

2
.gitignore vendored
View File

@ -5,5 +5,3 @@ result-*
*.qcow2
_build
*-secrets.nix
examples/static-leases.nix
/doc/hardware.rst

105
NEWS
View File

@ -1,105 +0,0 @@
A brief guide to backward-incompatible changes
that are likely to break configurations or workflows
2023-07-13
* a significant re-arrangement of modules and services, which will
probably break any configuration written before this time. For a
detailed explanation, see
https://www.liminix.org/doc/configuration.html#modules
2023-12-10
* configurations (usually) need no longer import modules from
modules/outputs because devices are expected to do this instead. This
change is because the outputs that make sense in any given context are
usually a property of the device being installed onto.
2023-12-11
* rename outputs.flashimage to outputs.mtdimage (and also diskimage to
mbrimage). This change is made in the expectation that "fooimage" is
the name of an outputs that gloms together other filesystem-like
outputs with some kind of partition table - so we might in future have
gptimage or lvmimage or ubimage.
2024-01-03
Liminix is now targeted to Nixpkgs 23.11 (not 23.05 as previously).
Upstream changes that have led to incompatible Liminix changes are:
* newer U-Boot version
* util-linux can now be built (previously depended on systemd)
2024-01-30
New port! Thanks to Arnout Engelen <arnout@bzzt.net>, Liminix
now runs on the TP-Link Archer AX23.
2024-02-12
* We now build wifi drivers (mac80211) from the same kernel source as
the running kernel, instead of using drivers from the linux-backports
project. This may be a regression on some devices that depend on
OpenWrt patches for wireless functionality: if you have a device that
used to work and now doesn't, refer to OpenWrt
package/kernel/mac80211/patches/ to see if there's something in there
that needs to be applied.
* in general, we build kernel modules (e.g. for nftables) at the same
time as the kernel itself instead of expecting to be able to build
them afterwards as though they were "out of tree". Refer to commit
b9c0d93670275e69df24902b05bf4aa4f0fcbe96 for a fuller explanation
of how this simplifies things.
2024-02-13
So that we can be more consistent about services that would like their
state to be preserved across boots (assuming a writable filesystem)
these changes have been made
* /run/service-state has been moved to /run/services/outputs
to better reflect what it's used for
* /run/services/state is either a symlink to /persist/services/state
(if there's a writeable fs on /persist) or a directory (if there
isn't)
The change will lose your ssh host key(s) unless you copy them from
the old location to the new one before rebooting into the new system
mkdir -m 02751 -p /run/services/state/dropbear
cp /persist/secrets/dropbear/* /run/services/state/dropbear
The `output`, `mkoutputs` functions defined by ${serviceFns}
have been updated for the new location.
2024-02-16
New (or at least, previously unreported) port! Liminix now runs on the
Turris Omnia and has been serving my family's internet needs for most
of this week. Thanks to NGI0 Entrust and the NLnet Foundation for
sponsoring this development (and funding the hardware)
2024-02-21
New port! Thanks to Raito Bezarius, Liminix now runs on the Zyxel NWA50AX,
an MT7621 (MIPS EL) dual radio WiFi AP.
2024-04-29
The setup for using `levitate` has changed: now it accepts an entire
config fragment, not just a list of services. Hopefully this makes it
a bit more useful :-)
defaultProfile.packages = with pkgs; [
...
(levitate.override {
config = {
services = {
inherit (config.services) dhcpc sshd watchdog;
};
defaultProfile.packages = [ mtdutils ];
users.root.openssh.authorizedKeys.keys = secrets.root.keys;
};
})
];

View File

@ -18,14 +18,22 @@ outside word goes across it.
Liminix is pre-1.0. We are still finding new and better ways to do things,
and there is no attempt to maintain backward compatibility with the old
ways.
ways. This will change when it settles down.
The [NEWS](NEWS) file (available wherever you found this README) is
a high-level overview of breaking changes.
_In general:_ development mostly happens on the `main` branch, which is
therefore not guaranteed to build or to work on every commit. For the
latest functioning version, see [the CI system](https://build.liminix.org/jobset/liminix/build) and pick a revision with all jobs green.
Development mostly happens on the `main` branch, which is therefore
not guaranteed to build or to work on every commit. For the latest
functioning version, see [the CI system](https://build.liminix.org/jobset/liminix/build) and pick a revision with all jobs green.
_In particular:_ as of July 2023, a significant re-arrangement of
modules and services is ongoing:
* if you are using out-of-tree configurations created before commit
2e50368, especially if they reference things under pkgs.liminix,
they will need updating. Look at changes to examples/rotuer.nix
for guidance
* the same is intermittently true for examples/{extensino,arhcive}.nix
where I've updated rotuer and not updated them to match.
## Documentation
@ -33,7 +41,7 @@ functioning version, see [the CI system](https://build.liminix.org/jobset/limini
Documentation is in the [doc](doc/) directory. You can build it
by running
nix-shell -p sphinx --run "make -C doc hardware.rst html"
nix-shell -p sphinx --run "make -C doc html"
Rendered documentation corresponding to the latest commit on `main`
is published to [https://www.liminix.org/doc/](https://www.liminix.org/doc/)

File diff suppressed because it is too large Load Diff

View File

@ -4,10 +4,6 @@ let
inherit (lib) mkOption mkEnableOption mdDoc types optional optionals;
in {
options.bordervm = {
keys = mkOption {
type = types.listOf types.str;
default = [];
};
l2tp = {
host = mkOption {
description = mdDoc ''
@ -103,17 +99,14 @@ in {
};
};
};
environment.systemPackages =
let wireshark-nogui = pkgs.wireshark.override { withQt = false ; };
in with pkgs; [
tcpdump
wireshark-nogui
socat
tufted
iptables
usbutils
busybox
];
environment.systemPackages = with pkgs; [
tcpdump
wireshark
socat
tufted
iptables
usbutils
];
security.sudo.wheelNeedsPassword = false;
networking = {
hostName = "border";
@ -127,7 +120,6 @@ in {
isNormalUser = true;
uid = 1000;
extraGroups = [ "wheel"];
openssh.authorizedKeys.keys = cfg.keys;
};
services.getty.autologinUser = "liminix";
};

69
ci.nix
View File

@ -8,16 +8,7 @@ let
pkgs = (import nixpkgs {});
borderVmConf = ./bordervm.conf-example.nix;
inherit (pkgs.lib.attrsets) genAttrs;
devices = [
"gl-ar750"
"gl-mt300a"
"gl-mt300n-v2"
"qemu"
"qemu-aarch64"
"qemu-armv7l"
"tp-archer-ax23"
"zyxel-nwa50ax"
];
devices = [ "qemu" "gl-ar750" "gl-mt300n-v2" "gl-mt300a" ];
vanilla = ./vanilla-configuration.nix;
for-device = name:
(import liminix {
@ -27,50 +18,32 @@ let
}).outputs.default;
tests = import ./tests/ci.nix;
jobs =
(genAttrs devices for-device) //
(genAttrs devices (name: for-device name)) //
tests //
{
buildEnv = (import liminix {
inherit nixpkgs borderVmConf;
inherit nixpkgs borderVmConf;
device = import (liminix + "/devices/qemu");
liminix-config = vanilla;
}).buildEnv;
doc =
let json =
(import liminix {
inherit nixpkgs borderVmConf;
device = import (liminix + "/devices/qemu");
liminix-config = {...} : {
imports = [ ./modules/all-modules.nix ];
};
}).outputs.optionsJson;
installers = map (f: "system.outputs.${f}") [
"vmroot"
"mtdimage"
"ubimage"
];
inherit (pkgs.lib) concatStringsSep;
in pkgs.stdenv.mkDerivation {
name = "liminix-doc";
nativeBuildInputs = with pkgs; [
gnumake sphinx fennel luaPackages.lyaml
];
src = ./.;
buildPhase = ''
cat ${json} | fennel --correlate doc/parse-options.fnl > doc/modules-generated.rst
cat ${json} | fennel --correlate doc/parse-options-outputs.fnl > doc/outputs-generated.rst
cp ${(import ./doc/hardware.nix)} doc/hardware.rst
make -C doc html
'';
installPhase = ''
mkdir -p $out/nix-support $out/share/doc/
cd doc
cp *-generated.rst $out
ln -s ${json} $out/options.json
cp -a _build/html $out/share/doc/liminix
echo "file source-dist \"$out/share/doc/liminix\"" \
> $out/nix-support/hydra-build-products
'';
doc = pkgs.stdenv.mkDerivation {
name = "liminix-doc";
nativeBuildInputs = with pkgs; [
gnumake sphinx
fennel luaPackages.lyaml
];
src = ./doc;
buildPhase = ''
cat ${(import ./doc/extract-options.nix).doc} | fennel --correlate parse-options.fnl > modules.rst
make html
'';
installPhase = ''
mkdir -p $out/nix-support $out/share/doc/
# (cd _build && tar cf $out/share/doc/liminix_manual.tar html)
cp -a _build/html $out/share/doc/liminix
echo "file source-dist \"$out/share/doc/liminix\"" \
> $out/nix-support/hydra-build-products
'';
};
with-unstable = (import liminix {
nixpkgs = unstable;

View File

@ -1,10 +1,8 @@
{
deviceName ? null
, device ? (import ./devices/${deviceName} )
device
, liminix-config ? <liminix-config>
, nixpkgs ? <nixpkgs>
, borderVmConf ? ./bordervm.conf.nix
, imageType ? "primary"
}:
let
@ -15,33 +13,24 @@ let
allowUnsupportedSystem = true; # mipsel
permittedInsecurePackages = [
"python-2.7.18.6" # kernel backports needs python <3
"python-2.7.18.7"
];
};
});
eval = pkgs.lib.evalModules {
specialArgs = {
modulesPath = builtins.toString ./modules;
};
config = (pkgs.lib.evalModules {
modules = [
{ _module.args = { inherit pkgs; inherit (pkgs) lim; }; }
{ _module.args = { inherit pkgs; lib = pkgs.lib; }; }
./modules/hardware.nix
./modules/base.nix
./modules/busybox.nix
./modules/hostname.nix
./modules/kernel
device.module
liminix-config
./modules/s6
./modules/users.nix
./modules/outputs.nix
{
boot.imageType = imageType;
}
];
};
config = eval.config;
}).config;
borderVm = ((import <nixpkgs/nixos/lib/eval-config.nix>) {
system = builtins.currentSystem;
@ -54,12 +43,6 @@ let
in {
outputs = config.system.outputs // {
default = config.system.outputs.${config.hardware.defaultOutput};
optionsJson =
let o = import ./doc/extract-options.nix {
inherit pkgs eval;
lib = pkgs.lib;
};
in pkgs.writeText "options.json" (builtins.toJSON o);
};
# this is just here as a convenience, so that we can get a
@ -71,13 +54,11 @@ in {
tufted
routeros.routeros
routeros.ros-exec-script
run-liminix-vm
mips-vm
borderVm.build.vm
go-l2tp
min-copy-closure
fennelrepl
lzma
lua
];
};
}

View File

@ -1,237 +0,0 @@
{
description = ''
Belkin RT-3200 / Linksys E8450
******************************
This device is based on a 64 bit Mediatek MT7622 ARM platform,
and is "work in progress" in Liminix.
.. note:: The factory flash image contains ECC errors that make it
incompatible with Liminix: you need to use the `OpenWrt
UBI Installer <https://github.com/dangowrt/owrt-ubi-installer>`_ to
rewrite the partition layout before you can flash
Liminix onto it (or even use it with
:ref:`system-outputs-tftpboot`, if you want the wireless
to work).
Hardware summary
================
- MediaTek MT7622BV (1350MHz)
- 128MB NAND flash
- 512MB RAM
- b/g/n wireless using MediaTek MT7622BV (MT7615E driver)
- a/n/ac/ax wireless using MediaTek MT7915E
Installation
============
Installation is currently a manual process (you need a :ref:`serial <serial>` conection and
TFTP) following the instructions at :ref:`system-outputs-ubimage`
'';
system = {
crossSystem = {
config = "aarch64-unknown-linux-musl";
};
};
module = {pkgs, config, lib, lim, ... }:
let firmware = pkgs.stdenv.mkDerivation {
name = "wlan-firmware";
phases = ["installPhase"];
installPhase = ''
mkdir $out
cp ${pkgs.linux-firmware}/lib/firmware/mediatek/{mt7915,mt7615,mt7622}* $out
'';
};
in {
imports = [
../../modules/arch/aarch64.nix
../../modules/outputs/tftpboot.nix
../../modules/outputs/ubifs.nix
];
config = {
kernel = {
src = pkgs.pkgsBuildBuild.fetchurl {
name = "linux.tar.gz";
url = "https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.137.tar.gz";
hash = "sha256-PkdzUKZ0IpBiWe/RS70J76JKnBFzRblWcKlaIFNxnHQ=";
};
extraPatchPhase = ''
${pkgs.openwrt.applyPatches.mediatek}
'';
config = {
PCI = "y";
ARCH_MEDIATEK = "y";
# ARM_MEDIATEK_CPUFREQ = "y";
# needed for "Cannot find regmap for /infracfg@10000000"
MFD_SYSCON = "y";
MTK_INFRACFG = "y";
MTK_PMIC_WRAP = "y";
NVMEM_MTK_EFUSE="y";
# MTK_HSDMA="y";
MTK_SCPSYS="y";
MTK_SCPSYS_PM_DOMAINS="y";
# MTK_THERMAL="y";
MTK_TIMER="y";
COMMON_CLK_MT7622 = "y";
COMMON_CLK_MT7622_ETHSYS = "y";
COMMON_CLK_MT7622_HIFSYS = "y";
COMMON_CLK_MT7622_AUDSYS = "y";
PM_CLK="y";
REGMAP_MMIO = "y";
CLKSRC_MMIO = "y";
REGMAP = "y";
MEDIATEK_GE_PHY = "y";
# MEDIATEK_MT6577_AUXADC = "y";
NET_MEDIATEK_SOC = "y";
NET_MEDIATEK_SOC_WED = "y";
NET_MEDIATEK_STAR_EMAC = "y"; # this enables REGMAP_MMIO
NET_VENDOR_MEDIATEK = "y";
PCIE_MEDIATEK = "y";
BLOCK = "y"; # move this to base option
SPI_MASTER = "y";
SPI = "y";
SPI_MEM="y";
SPI_MTK_NOR="y";
SPI_MTK_SNFI = "y";
MTD = "y";
MTD_BLOCK = "y";
MTD_RAW_NAND = "y";
MTD_NAND_MTK = "y";
MTD_NAND_MTK_BMT = "y"; # Bad-block Management Table
MTD_NAND_ECC_MEDIATEK= "y";
MTD_NAND_ECC_SW_HAMMING= "y";
MTD_SPI_NAND= "y";
MTD_OF_PARTS = "y";
MTD_NAND_CORE= "y";
MTD_SPI_NOR= "y";
MTD_SPLIT_FIRMWARE= "y";
MTD_SPLIT_FIT_FW= "y";
MMC = "y";
MMC_BLOCK = "y";
MMC_CQHCI = "y";
MMC_MTK = "y";
# Distributed Switch Architecture is needed
# to make the ethernet ports visible
NET_DSA="y";
NET_DSA_MT7530="y";
NET_DSA_TAG_MTK="y";
PSTORE = "y";
PSTORE_RAM = "y";
PSTORE_CONSOLE = "y";
PSTORE_DEFLATE_COMPRESS = "n";
SERIAL_8250 = "y";
SERIAL_8250_CONSOLE = "y";
SERIAL_8250_MT6577="y";
# SERIAL_8250_NR_UARTS="3";
# SERIAL_8250_RUNTIME_UARTS="3";
SERIAL_OF_PLATFORM="y";
# Must enble hardware watchdog drivers. Else the device reboots after several seconds
WATCHDOG = "y";
MEDIATEK_WATCHDOG = "y";
};
conditionalConfig = {
WLAN= {
MT7615E = "m";
MT7622_WMAC = "y";
MT7915E = "m";
};
};
};
boot = {
commandLine = [ "console=ttyS0,115200" ];
tftp.loadAddress = lim.parseInt "0x4007ff28";
imageFormat = "fit";
};
filesystem =
let inherit (pkgs.pseudofile) dir symlink;
in
dir {
lib = dir {
firmware = dir {
mediatek = symlink firmware;
};
};
};
hardware =
let
openwrt = pkgs.openwrt;
mac80211 = pkgs.kmodloader.override {
targets = ["mt7615e" "mt7915e"];
inherit (config.system.outputs) kernel;
};
in {
ubi = {
minIOSize = "2048";
eraseBlockSize = "126976";
maxLEBcount = "1024"; # guessing
};
defaultOutput = "ubimage";
# the kernel expects this to be on a 2MB boundary. U-Boot
# (I don't know why) has a default of 0x41080000, which isn't.
# We put it at the 32MB mark so that tftpboot can put its rootfs
# image and DTB underneath, but maybe this is a terrible waste of
# RAM unless the kernel is able to reuse it later. Oh well
loadAddress = lim.parseInt "0x42000000";
entryPoint = lim.parseInt "0x42000000";
rootDevice = "ubi0:liminix";
dts = {
src = "${openwrt.src}/target/linux/mediatek/dts/mt7622-linksys-e8450-ubi.dts";
includes = [
"${openwrt.src}/target/linux/mediatek/dts"
"${config.system.outputs.kernel.modulesupport}/arch/arm64/boot/dts/mediatek/"
];
};
# - 0x000000000000-0x000008000000 : "spi-nand0"
# - 0x000000000000-0x000000080000 : "bl2"
# - 0x000000080000-0x0000001c0000 : "fip"
# - 0x0000001c0000-0x0000002c0000 : "factory"
# - 0x0000002c0000-0x000000300000 : "reserved"
# - 0x000000300000-0x000008000000 : "ubi"
networkInterfaces =
let
inherit (config.system.service.network) link;
inherit (config.system.service) bridge;
in rec {
wan = link.build { ifname = "wan"; };
lan1 = link.build { ifname = "lan1"; };
lan2 = link.build { ifname = "lan2"; };
lan3 = link.build { ifname = "lan3"; };
lan4 = link.build { ifname = "lan4"; };
lan = lan3;
wlan = link.build {
ifname = "wlan0";
dependencies = [ mac80211 ];
};
wlan5 = link.build {
ifname = "wlan1";
dependencies = [ mac80211 ];
};
};
};
};
};
}

View File

@ -1,57 +0,0 @@
{ config, pkgs, ... }:
{
imports = [
../../modules/outputs/jffs2.nix
];
config = {
kernel = {
src = pkgs.pkgsBuildBuild.fetchurl {
name = "linux.tar.gz";
url = "https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.137.tar.gz";
hash = "sha256-PkdzUKZ0IpBiWe/RS70J76JKnBFzRblWcKlaIFNxnHQ=";
};
config = {
MTD = "y";
MTD_BLOCK = "y";
MTD_CMDLINE_PARTS = "y";
MTD_PHRAM = "y";
VIRTIO_MENU = "y";
PCI = "y";
VIRTIO_PCI = "y";
BLOCK = "y";
VIRTIO_BLK = "y";
VIRTIO_NET = "y";
};
};
hardware =
let
mac80211 = pkgs.mac80211.override {
drivers = ["mac80211_hwsim"];
klibBuild = config.system.outputs.kernel.modulesupport;
};
in {
defaultOutput = "vmroot";
rootDevice = "/dev/mtdblock0";
dts.src = pkgs.lib.mkDefault null;
flash.eraseBlockSize = 65536;
networkInterfaces =
let inherit (config.system.service.network) link;
in {
wan = link.build {
devpath = "/devices/pci0000:00/0000:00:13.0/virtio0";
ifname = "wan";
};
lan = link.build {
devpath = "/devices/pci0000:00/0000:00:14.0/virtio1";
ifname = "lan";
};
wlan_24 = link.build {
ifname = "wlan0";
dependencies = [ mac80211 ];
};
};
};
};
}

View File

@ -1,3 +1,11 @@
# I like GL.iNet devices because they're relatively accessible to
# DIY users: the serial port connections have headers preinstalled
# and don't need soldering
# Mainline linux 5.19 doesn't have device-tree support for this device
# or even for the SoC, so we use the extensive OpenWrt kernel patches
{
system = {
crossSystem = {
@ -10,49 +18,27 @@
};
description = ''
GL.iNet GL-AR750
****************
Hardware summary
================
The GL-AR750 "Creta" travel router features:
GL.INet GL-AR750 "Creta" travel router
- QCA9531 @650Mhz SoC
- dual band wireless: IEEE 802.11a/b/g/n/ac
- two 10/100Mbps LAN ports and one WAN
- 128MB DDR2 RAM
- 16MB NOR Flash
- supported in OpenWrt by the "ath79" SoC family
- 128MB DDR2 RAM / 16MB NOR Flash
- "ath79" soc family
https://www.gl-inet.com/products/gl-ar750/
The GL-AR750 has two distinct sets of wifi hardware. The 2.4GHz
radio is part of the QCA9531 SoC, i.e. it's on the same silicon as
the CPU, the Ethernet, the USB etc. The device is connected to the
host via `AHB <https://en.wikipedia.org/wiki/Advanced_Microcontroller_Bus_Architecture>`_ and it is
supported in Linux using the ath9k driver. 5GHz wifi
is provided by a QCA9887 PCIe (PCI embedded) WLAN chip,
host via AHB, the "Advanced High-Performance Bus" and it is
supported in Linux using the ath9k driver. The 5GHz support, on the
other hand, is provided by a QCA9887 PCIe (PCI embedded) WLAN chip:
I haven't looked closely at the router innards to see if this is
actually physically a separate board that could be unplugged, but
as far as the Linux is concerned it behaves as one. This is
supported by the ath10k driver.
Installation
============
As with many GL.iNet devices, the stock vendor firmware
is a fork of OpenWrt, meaning that the binary created by
:ref:`system-outputs-mtdimage` can be flashed using the
vendor web UI or the U-Boot emergency "unbrick" routine.
For flashing from an existing Liminix system (we believe that) it
is necessary to first boot into a :ref:`system-outputs-kexecboot`
system, otherwise you'll be overwriting flash partitions while
they're in use - and that might not end well.
Vendor web page: https://www.gl-inet.com/products/gl-ar750/
OpenWrt web page: https://openwrt.org/toh/gl.inet/gl-ar750
'';
module = {pkgs, config, lim, ... }:
module = {pkgs, config, ... }:
let
openwrt = pkgs.openwrt;
firmwareBlobs = pkgs.pkgsBuildBuild.fetchFromGitHub {
@ -71,15 +57,14 @@
cp $blobdir/board.bin $out/ath10k/QCA9887/hw1.0/
'';
};
mac80211 = pkgs.kmodloader.override {
targets = ["ath9k" "ath10k_pci"];
inherit (config.system.outputs) kernel;
dependencies = [ ath10k_cal_data ];
mac80211 = pkgs.mac80211.override {
drivers = ["ath9k" "ath10k_pci"];
klibBuild = config.system.outputs.kernel.modulesupport;
};
ath10k_cal_data =
let
offset = lim.parseInt "0x5000";
size = lim.parseInt "0x844";
offset = 1024 * 20; # 0x5000
size = 2048 + 68; # 0x844
in pkgs.liminix.services.oneshot rec {
name = "ath10k_cal_data";
up = ''
@ -90,29 +75,22 @@
dd if=/dev/$part of=data iflag=skip_bytes,fullblock bs=${toString size} skip=${toString offset} count=1
)
'';
down = "true";
};
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs.liminix.networking) interface;
in {
imports = [
../../modules/network
../../modules/arch/mipseb.nix
../../modules/outputs/tftpboot.nix
../../modules/outputs/mtdimage.nix
../../modules/outputs/jffs2.nix
];
programs.busybox.options = {
FEATURE_DD_IBS_OBS = "y"; # ath10k_cal_data needs skip_bytes,fullblock
};
hardware = {
defaultOutput = "mtdimage";
loadAddress = lim.parseInt "0x80060000";
entryPoint = lim.parseInt "0x80060000";
defaultOutput = "tftpboot";
loadAddress = "0x80060000";
entryPoint = "0x80060000";
flash = {
address = lim.parseInt "0x9F060000";
size = lim.parseInt "0xfa0000";
eraseBlockSize = 65536;
address = "0x9F060000";
size ="0xfa0000";
eraseBlockSize = "65536";
};
rootDevice = "/dev/mtdblock5";
dts = {
@ -122,20 +100,19 @@
];
};
networkInterfaces =
let inherit (config.system.service.network) link;
in {
lan = link.build { ifname = "eth0"; };
wan = link.build { ifname = "eth1"; };
wlan = link.build {
ifname = "wlan0";
dependencies = [ mac80211 ];
};
wlan5 = link.build {
ifname = "wlan1";
dependencies = [ ath10k_cal_data mac80211 ];
};
networkInterfaces = {
lan = interface { device = "eth0"; };
wan = interface { device = "eth1"; };
wlan_24 = interface {
device = "wlan0";
dependencies = [ mac80211 ];
};
wlan_5 = interface {
device = "wlan1";
dependencies = [ mac80211 ath10k_cal_data ];
};
};
};
filesystem = dir {
lib = dir {
@ -148,25 +125,21 @@
};
};
boot.tftp = {
loadAddress = lim.parseInt "0x00A00000";
loadAddress = "0x00A00000";
};
kernel = {
src = pkgs.pkgsBuildBuild.fetchurl {
name = "linux.tar.gz";
url = "https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.137.tar.gz";
hash = "sha256-PkdzUKZ0IpBiWe/RS70J76JKnBFzRblWcKlaIFNxnHQ=";
url = "https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.71.tar.gz";
hash = "sha256-yhO2cXIeIgUxkSZf/4aAsF11uxyh+UUZu6D1h92vCD8=";
};
# Mainline linux 5.19 doesn't have device-tree support for
# this device or even for the SoC, so we use the extensive
# OpenWrt kernel patches
extraPatchPhase = ''
${openwrt.applyPatches.ath79}
sed -i.bak -e '\,include <linux/hw_random.h>,a #include <linux/gpio/driver.h>' drivers/net/wireless/ath/ath9k/ath9k.h # context reqd for next patch
patch -p1 < ${openwrt.src}/package/kernel/mac80211/patches/ath9k/552-ath9k-ahb_of.patch
'';
config = {
MIPS_ELF_APPENDED_DTB = "y";
OF = "y";
USE_OF = "y";
ATH79 = "y";
PCI = "y";
PCI_AR724X = "y";
@ -186,6 +159,7 @@
CONSOLE_LOGLEVEL_QUIET = "4";
NET = "y";
NETDEVICES = "y";
ETHERNET = "y";
NET_VENDOR_ATHEROS = "y";
AG71XX = "y"; # ethernet (qca,qca9530-eth)
@ -193,6 +167,7 @@
AR8216_PHY = "y"; # eth1 is behind a switch
MTD_SPI_NOR = "y";
MTD_SPI_NOR_USE_4K_SECTORS = "n"; # jffs2 needs min 8k erase block
SPI_ATH79 = "y"; # these are copied from OpenWrt.
SPI_MASTER= "y"; # At least one of them is necessary
@ -209,25 +184,25 @@
SYSFS = "y";
SPI = "y";
MTD = "y";
MTD_CMDLINE_PARTS = "y";
MTD_BLOCK = "y"; # fix undefined ref to register_mtd_blktrans_devs
WATCHDOG = "y";
ATH79_WDT = "y"; # watchdog timer
CPU_BIG_ENDIAN= "y";
# this is all copied from nixwrt ath79 config. Clearly not all
# of it is device config, some of it is wifi config or
# installation method config or ...
CMDLINE_PARTITION = "y";
EARLY_PRINTK = "y";
PARTITION_ADVANCED = "y";
PRINTK_TIME = "y";
};
conditionalConfig = {
WLAN = {
WLAN_VENDOR_ATH = "y";
ATH_COMMON = "m";
ATH9K = "m";
ATH9K_AHB = "y";
ATH10K = "m";
ATH10K_PCI = "m";
ATH10K_DEBUG = "y";
};
SQUASHFS = "y";
SQUASHFS_XZ = "y";
};
};
};

View File

@ -1,4 +1,4 @@
# GL.iNet GL-MT300A
# GL.INet GL-MT300A
{
system = {
@ -12,56 +12,25 @@
};
description = ''
GL.iNet GL-MT300A
*****************
The GL-MT300A is based on a MT7620 chipset.
For flashing from U-Boot, the firmware partition is from
0xbc050000 to 0xbcfd0000.
WiFi on this device is provided by the rt2800soc module. It
expects firmware to be present in the "factory" MTD partition, so
- assuming we want to use the wireless - we need to build MTD
support into the kernel even if we're using TFTP root.
Installation
============
The stock vendor firmware is a fork of OpenWrt, meaning that the
binary created by :ref:`system-outputs-mtdimage` can be flashed
using the vendor web UI or the U-Boot emergency "unbrick" routine.
For flashing from an existing Liminix system (we think) it
is necessary to first boot into a :ref:`system-outputs-kexecboot`
system, otherwise you'll be overwriting flash partitions while
they're in use - and that might not end well.
Vendor web page: https://www.gl-inet.com/products/gl-mt300a/
OpenWrt web page: https://openwrt.org/toh/gl.inet/gl-mt300a
support into the kernel even if we're using TFTP root
'';
module = { pkgs, config, lib, lim, ...}:
module = { pkgs, config, ...}:
let
inherit (pkgs.liminix.networking) interface;
inherit (pkgs) openwrt;
mac80211 = pkgs.kmodloader.override {
targets = ["rt2800soc"];
inherit (config.system.outputs) kernel;
mac80211 = pkgs.mac80211.override {
drivers = ["rt2800soc"];
klibBuild = config.system.outputs.kernel.modulesupport;
};
in {
imports = [
../../modules/arch/mipsel.nix
../../modules/outputs/tftpboot.nix
../../modules/outputs/mtdimage.nix
../../modules/outputs/jffs2.nix
];
in {
hardware = {
defaultOutput = "mtdimage";
loadAddress = lim.parseInt "0x80000000";
entryPoint = lim.parseInt "0x80000000";
defaultOutput = "tftpboot";
loadAddress = "0x80000000";
entryPoint = "0x80000000";
# Creating 5 MTD partitions on "spi0.0":
# 0x000000000000-0x000000030000 : "u-boot"
@ -74,9 +43,9 @@
# 0x000000260000-0x000000f80000 : "rootfs"
flash = {
address = lim.parseInt "0xbc050000";
size = lim.parseInt "0xf80000";
eraseBlockSize = 65536;
address = "0xbc050000";
size ="0xf80000";
eraseBlockSize = "65536";
};
rootDevice = "/dev/mtdblock5";
@ -86,58 +55,46 @@
"${openwrt.src}/target/linux/ramips/dts"
];
};
networkInterfaces =
let
inherit (config.system.service.network) link;
inherit (config.system.service) vlan;
inherit (pkgs.liminix.services) oneshot;
swconfig = oneshot {
name = "swconfig";
up = ''
PATH=${pkgs.swconfig}/bin:$PATH
swconfig dev switch0 set reset
swconfig dev switch0 set enable_vlan 1
swconfig dev switch0 vlan 1 set ports '1 2 3 4 6t'
swconfig dev switch0 vlan 2 set ports '0 6t'
swconfig dev switch0 set apply
'';
down = "${pkgs.swconfig}/bin/swconfig dev switch0 set reset";
};
in rec {
eth = link.build { ifname = "eth0"; };
# lan and wan ports are both behind a switch on eth0
lan = vlan.build {
ifname = "eth0.1";
primary = eth;
vid = "1";
};
wan = vlan.build {
ifname = "eth0.2";
primary = eth;
vid = "2";
};
wlan = link.build {
ifname = "wlan0";
dependencies = [ mac80211 ];
};
networkInterfaces = rec {
# lan and wan ports are both behind a switch on eth0
eth = interface { device = "eth0"; };
lan = interface {
type = "vlan";
device = "eth0.1";
link = "eth0";
id = "1";
dependencies = [eth];
};
wan = interface {
type = "vlan";
device = "eth0.2";
id = "2";
link = "eth0";
dependencies = [eth];
};
wlan = interface {
device = "wlan0";
dependencies = [ mac80211 ];
};
};
};
boot.tftp = {
loadAddress = lim.parseInt "0x00A00000";
appendDTB = true;
};
loadAddress = "0x00A00000";
};
kernel = {
src = pkgs.fetchurl {
name = "linux.tar.gz";
url = "https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.137.tar.gz";
hash = "sha256-PkdzUKZ0IpBiWe/RS70J76JKnBFzRblWcKlaIFNxnHQ=";
url = "https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.71.tar.gz";
hash = "sha256-yhO2cXIeIgUxkSZf/4aAsF11uxyh+UUZu6D1h92vCD8=";
};
extraPatchPhase = ''
${openwrt.applyPatches.ramips}
${openwrt.applyPatches.rt2x00}
'';
config = {
MIPS_ELF_APPENDED_DTB = "y";
OF = "y";
USE_OF = "y";
RALINK = "y";
PCI = "y";
@ -152,12 +109,12 @@
CONSOLE_LOGLEVEL_QUIET = "4";
NET = "y";
NETDEVICES = "y";
ETHERNET = "y";
NET_VENDOR_RALINK = "y";
NET_RALINK_MDIO = "y";
NET_RALINK_MDIO_MT7620 = "y";
NET_RALINK_MT7620 = "y";
SWPHY = "y";
SPI = "y";
MTD_SPI_NOR = "y";
@ -166,26 +123,35 @@
SPI_MASTER= "y";
SPI_MEM= "y";
# both the ethernet ports on this device (lan and wan)
# are behind a switch, so we need VLANs to do anything
# useful with them
VLAN_8021Q = "y";
SWCONFIG = "y";
SWPHY = "y";
BRIDGE = "y";
BRIDGE_VLAN_FILTERING = "y";
BRIDGE_IGMP_SNOOPING = "y";
MTD = "y";
MTD_CMDLINE_PARTS = "y";
MTD_BLOCK = "y"; # fix undefined ref to register_mtd_blktrans_devs
CPU_LITTLE_ENDIAN = "y";
CMDLINE_PARTITION = "y";
EARLY_PRINTK = "y";
NEW_LEDS = "y";
LEDS_CLASS = "y"; # required by rt2x00lib
PARTITION_ADVANCED = "y";
PRINTK_TIME = "y";
} // lib.optionalAttrs (config.system.service ? vlan) {
SWCONFIG = "y";
SQUASHFS = "y";
SQUASHFS_XZ = "y";
};
conditionalConfig = {
WLAN = {
WLAN_VENDOR_RALINK = "y";
RT2800SOC = "m";
RT2X00 = "m";
};
};
};
};
}

View File

@ -1,3 +1,5 @@
# GL.INet GL-MT300N v2
{
system = {
crossSystem = {
@ -9,55 +11,22 @@
};
};
description = ''
GL.iNet GL-MT300N-v2
********************
The GL-MT300N-v2 "Mango" is is very similar to the :ref:`MT300A <GL.iNet GL-MT300A>, but is
based on the MT7628 chipset instead of MT7620. It's also marginally cheaper
and comes in a yellow case not a blue one. Be sure your device is
v2 not v1, which is a different animal and has only half as much RAM.
Installation
============
The stock vendor firmware is a fork of OpenWrt, meaning that the
binary created by :ref:`system-outputs-mtdimage` can be flashed
using the vendor web UI or the U-Boot emergency "unbrick" routine.
For flashing from an existing Liminix system (we think) it
is necessary to first boot into a :ref:`system-outputs-kexecboot`
system, otherwise you'll be overwriting flash partitions while
they're in use - and that might not end well.
Vendor web page: https://www.gl-inet.com/products/gl-mt300n-v2/
OpenWrt web page: https://openwrt.org/toh/gl.inet/gl-mt300n_v2
'';
module = { pkgs, config, lib, lim, ...}:
module = { pkgs, config, ...}:
let
inherit (pkgs.liminix.networking) interface;
inherit (pkgs.liminix.services) oneshot;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) openwrt;
mac80211 = pkgs.kmodloader.override {
targets = ["mt7603e"];
inherit (config.system.outputs) kernel;
mac80211 = pkgs.mac80211.override {
drivers = ["mt7603e"];
klibBuild = config.system.outputs.kernel.modulesupport;
};
wlan_firmware = pkgs.fetchurl {
url = "https://github.com/openwrt/mt76/raw/f24b56f935392ca1d35fae5fd6e56ef9deda4aad/firmware/mt7628_e2.bin";
hash = "sha256:1dkhfznmdz6s50kwc841x3wj0h6zg6icg5g2bim9pvg66as2vmh9";
};
in {
imports = [
../../modules/arch/mipsel.nix
../../modules/outputs/tftpboot.nix
../../modules/outputs/mtdimage.nix
../../modules/outputs/jffs2.nix
];
filesystem = dir {
lib = dir {
firmware = dir {
@ -66,14 +35,14 @@
};
};
hardware = {
defaultOutput = "mtdimage";
loadAddress = lim.parseInt "0x80000000";
entryPoint = lim.parseInt "0x80000000";
defaultOutput = "tftpboot";
loadAddress = "0x80000000";
entryPoint = "0x80000000";
flash = {
address = lim.parseInt "0xbc050000";
size = lim.parseInt "0xfb0000";
eraseBlockSize = 65536;
address = "0xbc050000";
size = "0xfb0000";
eraseBlockSize = "65536";
};
rootDevice = "/dev/mtdblock5";
@ -83,62 +52,69 @@
"${openwrt.src}/target/linux/ramips/dts"
];
};
networkInterfaces =
let
inherit (config.system.service.network) link;
inherit (config.system.service) vlan;
swconfig = oneshot {
name = "swconfig";
up = ''
PATH=${pkgs.swconfig}/bin:$PATH
swconfig dev switch0 set reset
swconfig dev switch0 set enable_vlan 1
swconfig dev switch0 vlan 1 set ports '1 2 3 4 6t'
swconfig dev switch0 vlan 2 set ports '0 6t'
swconfig dev switch0 set apply
'';
down = "${pkgs.swconfig}/bin/swconfig dev switch0 set reset";
};
in rec {
eth = link.build { ifname = "eth0"; dependencies = [swconfig]; };
# lan and wan ports are both behind a switch on eth0
lan = vlan.build {
ifname = "eth0.1";
primary = eth;
vid = "1";
};
wan = vlan.build {
ifname = "eth0.2";
primary = eth;
vid = "2";
};
wlan = link.build {
ifname = "wlan0";
dependencies = [ mac80211 ];
networkInterfaces = rec {
# lan and wan ports are both behind a switch on eth0
eth =
let swconfig = oneshot {
name = "swconfig";
up = ''
PATH=${pkgs.swconfig}/bin:$PATH
swconfig dev switch0 set reset
swconfig dev switch0 set enable_vlan 1
swconfig dev switch0 vlan 1 set ports '1 2 3 4 6t'
swconfig dev switch0 vlan 2 set ports '0 6t'
swconfig dev switch0 set apply
'';
down = "swconfig dev switch0 set reset";
};
in interface {
device = "eth0";
dependencies = [swconfig];
};
lan = interface {
type = "vlan";
device = "eth0.1";
link = "eth0";
id = "1";
dependencies = [eth];
};
wan = interface {
type = "vlan";
device = "eth0.2";
id = "2";
link = "eth0";
dependencies = [eth];
};
wlan = interface {
device = "wlan0";
dependencies = [ mac80211 ];
};
};
};
boot.tftp = {
# 20MB seems to give enough room to uncompress the kernel
# without anything getting trodden on. 10MB was too small
loadAddress = lim.parseInt "0x1400000";
appendDTB = true;
loadAddress = "0x1400000";
};
kernel = {
src = pkgs.fetchurl {
name = "linux.tar.gz";
url = "https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.137.tar.gz";
hash = "sha256-PkdzUKZ0IpBiWe/RS70J76JKnBFzRblWcKlaIFNxnHQ=";
url = "https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.71.tar.gz";
hash = "sha256-yhO2cXIeIgUxkSZf/4aAsF11uxyh+UUZu6D1h92vCD8=";
};
extraPatchPhase = ''
${openwrt.applyPatches.ramips}
'';
config = {
MIPS_ELF_APPENDED_DTB = "y";
OF = "y";
USE_OF = "y";
RALINK = "y";
PCI = "y";
SOC_MT7620 = "y";
CPU_LITTLE_ENDIAN= "y";
SERIAL_8250_CONSOLE = "y";
SERIAL_8250 = "y";
@ -149,6 +125,7 @@
CONSOLE_LOGLEVEL_QUIET = "4";
MTD = "y";
MTD_CMDLINE_PARTS = "y";
MTD_BLOCK = "y"; # fix undefined ref to register_mtd_blktrans_dev
SPI = "y";
@ -161,6 +138,7 @@
REGULATOR_FIXED_VOLTAGE = "y";
NET = "y";
NETDEVICES = "y";
ETHERNET = "y";
PHYLIB = "y";
@ -170,31 +148,36 @@
NET_VENDOR_RALINK = "y";
NET_RALINK_RT3050 = "y";
NET_RALINK_SOC="y";
# both the ethernet ports on this device (lan and wan)
# are behind a switch, so we need VLANs to do anything
# useful with them
VLAN_8021Q = "y";
SWCONFIG = "y";
SWPHY = "y";
BRIDGE = "y";
BRIDGE_VLAN_FILTERING = "y";
BRIDGE_IGMP_SNOOPING = "y";
WATCHDOG = "y";
RALINK_WDT = "y"; # watchdog
MT7621_WDT = "y"; # or it might be this one
GPIOLIB="y";
GPIO_MT7621 = "y";
PHY_RALINK_USB = "y";
CMDLINE_PARTITION = "y";
EARLY_PRINTK = "y";
PARTITION_ADVANCED = "y";
PRINTK_TIME = "y";
} // lib.optionalAttrs (config.system.service ? vlan) {
SWCONFIG = "y";
} // lib.optionalAttrs (config.system.service ? watchdog) {
RALINK_WDT = "y"; # watchdog
MT7621_WDT = "y"; # or it might be this one
SQUASHFS = "y";
SQUASHFS_XZ = "y";
};
conditionalConfig = {
WLAN = {
WLAN_VENDOR_RALINK = "y";
WLAN_VENDOR_MEDIATEK = "y";
MT7603E = "m";
};
};
};
};
}

View File

@ -1,51 +0,0 @@
# This "device" generates images that can be used with the QEMU
# emulator. The default output is a directory containing separate
# kernel ("Image" format) and root filesystem (squashfs or jffs2)
# images
{
system = {
crossSystem = {
config = "aarch64-unknown-linux-musl";
};
};
description = ''
QEMU Aarch64
************
This target produces an image for
the `QEMU "virt" platform <https://www.qemu.org/docs/master/system/arm/virt.html>`_ using a 64 bit CPU type.
ARM targets differ from MIPS in that the kernel format expected
by QEMU is an "Image" (raw binary file) rather than an ELF
file, but this is taken care of by :command:`run.sh`. Check the
documentation for the :ref:`QEMU` (MIPS) target for more information.
'';
# this device is described by the "qemu" device
installer = "vmroot";
module = {pkgs, config, lim, ... }: {
imports = [
../../modules/arch/aarch64.nix
../families/qemu.nix
];
kernel = {
config = {
VIRTUALIZATION = "y";
PCI_HOST_GENERIC="y";
SERIAL_AMBA_PL011 = "y";
SERIAL_AMBA_PL011_CONSOLE = "y";
};
};
boot.commandLine = [
"console=ttyAMA0,38400"
];
hardware = let addr = lim.parseInt "0x40010000"; in {
loadAddress = addr;
entryPoint = addr;
};
};
}

View File

@ -1,53 +0,0 @@
# This "device" generates images that can be used with the QEMU
# emulator. The default output is a directory containing separate
# kernel ("Image" format) and root filesystem (squashfs or jffs2)
# images
{
system = {
crossSystem = {
config = "armv7l-unknown-linux-musleabihf";
};
};
# this device is described by the "qemu" device
description = ''
QEMU ARM v7
***********
This target produces an image for
the `QEMU "virt" platform <https://www.qemu.org/docs/master/system/arm/virt.html>`_ using a 32 bit CPU type.
ARM targets differ from MIPS in that the kernel format expected
by QEMU is an "Image" (raw binary file) rather than an ELF
file, but this is taken care of by :command:`run.sh`. Check the
documentation for the :ref:`QEMU` (MIPS) target for more information.
'';
installer = "vmroot";
module = {pkgs, config, lim, ... }: {
imports = [
../../modules/arch/arm.nix
../families/qemu.nix
];
kernel = {
config = {
PCI_HOST_GENERIC = "y";
ARCH_VIRT = "y";
VFP = "y";
NEON = "y";
AEABI = "y";
SERIAL_AMBA_PL011 = "y";
SERIAL_AMBA_PL011_CONSOLE = "y";
};
};
boot.commandLine = [
"console=ttyAMA0"
];
hardware = let addr = lim.parseInt "0x40008000"; in {
loadAddress = addr;
entryPoint = addr;
};
};
}

View File

@ -12,65 +12,59 @@
};
};
description = ''
QEMU MIPS
*********
This target produces an image for
QEMU, the "generic and open source machine emulator and
virtualizer".
MIPS QEMU emulates a "Malta" board, which was an ATX form factor
evaluation board made by MIPS Technologies, but mostly in Liminix
we use paravirtualized devices (Virtio) instead of emulating
hardware.
Building an image for QEMU results in a :file:`result/` directory
containing ``run.sh`` ``vmlinux``, and ``rootfs`` files. To invoke
the emulator, run ``run.sh``.
The configuration includes two emulated "hardware" ethernet
devices and the kernel :code:`mac80211_hwsim` module to
provide an emulated wlan device. To read more about how
to connect to this network, refer to :ref:`qemu-networking`
in the Development manual.
'';
module = {pkgs, config, lib, lim, ... }: {
imports = [
../../modules/arch/mipseb.nix
../families/qemu.nix
];
module = {pkgs, config, ... }: {
kernel = {
src = pkgs.pkgsBuildBuild.fetchurl {
name = "linux.tar.gz";
url = "https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.71.tar.gz";
hash = "sha256-yhO2cXIeIgUxkSZf/4aAsF11uxyh+UUZu6D1h92vCD8=";
};
config = {
MIPS_MALTA= "y";
CPU_LITTLE_ENDIAN= "n";
CPU_BIG_ENDIAN= "y";
CPU_MIPS32_R2= "y";
POWER_RESET = "y";
POWER_RESET_SYSCON = "y";
MTD = "y";
MTD_BLOCK2MTD = "y";
MTD_BLKDEVS = "y";
MTD_BLOCK = "y";
SQUASHFS = "y";
SQUASHFS_XZ = "y";
VIRTIO_MENU = "y";
PCI = "y";
VIRTIO_PCI = "y";
BLOCK = "y";
VIRTIO_BLK = "y";
NETDEVICES = "y";
VIRTIO_NET = "y";
SERIAL_8250= "y";
SERIAL_8250_CONSOLE= "y";
};
};
hardware =
# from arch/mips/mti-malta/Platform:load-$(CONFIG_MIPS_MALTA) += 0xffffffff80100000
let addr = lim.parseInt "0x80100000";
in {
loadAddress = addr;
entryPoint = addr;
# Unlike the arm qemu targets, we need a static dts when
# running u-boot-using tests, qemu dumpdtb command doesn't
# work for this board. I am not at all sure this dts is
# *correct* but it does at least boot
dts = lib.mkForce {
src = "${config.system.outputs.kernel.modulesupport}/arch/mips/boot/dts/mti/malta.dts";
includes = [
"${config.system.outputs.kernel.modulesupport}/arch/mips/boot/dts/"
];
let
mac80211 = pkgs.mac80211.override {
drivers = ["mac80211_hwsim"];
klibBuild = config.system.outputs.kernel.modulesupport;
};
inherit (pkgs.liminix.networking) interface;
in {
defaultOutput = "vmroot";
flash.eraseBlockSize = "65536"; # c.f. pkgs/mips-vm/mips-vm.sh
networkInterfaces = {
lan = interface { device = "eth0"; };
wan = interface { device = "eth1"; };
wlan_24 = interface {
device = "wlan0";
dependencies = [ mac80211 ];
};
};
};
};
}

View File

@ -1,442 +0,0 @@
{
description = ''
TP-Link Archer AX23 / AX1800 Dual Band Wi-Fi 6 Router
*****************************************************
Hardware summary
================
- MediaTek MT7621 (880MHz)
- 16MB Flash
- 128MB RAM
- WLan hardware: Mediatek MT7905, MT7975
Limitations
===========
Status LEDs do not work yet.
Uploading an image via tftp doesn't work yet, because the Archer uboot
version is so old it doesn't support overriding the DTB from the mboot
command. The tftpboot module doesn't support this yet, see
https://gti.telent.net/dan/liminix/pulls/5 for the WiP.
'';
system = {
crossSystem = {
config = "mipsel-unknown-linux-musl";
gcc = {
abi = "32";
# https://openwrt.org/docs/techref/instructionset/mipsel_24kc
arch = "24kc";
};
};
};
module = {pkgs, config, lib, lim, ... }:
let firmware = pkgs.stdenv.mkDerivation {
name = "wlan-firmware";
phases = ["installPhase"];
installPhase = ''
mkdir $out
cp ${pkgs.linux-firmware}/lib/firmware/mediatek/{mt7915,mt7615,mt7622}* $out
'';
};
in {
imports = [
../../modules/arch/mipsel.nix
../../modules/outputs/tftpboot.nix
../../modules/outputs/tplink-safeloader.nix
];
config = {
kernel = {
src = pkgs.pkgsBuildBuild.fetchurl {
name = "linux.tar.gz";
url = "https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.137.tar.gz";
hash = "sha256-PkdzUKZ0IpBiWe/RS70J76JKnBFzRblWcKlaIFNxnHQ=";
};
extraPatchPhase = ''
${pkgs.openwrt.applyPatches.ramips}
'';
config = {
# Initially taken from openwrt's ./target/linux/ramips/mt7621/config-5.15,
# then tweaked here and there
ARCH_32BIT_OFF_T="y";
ARCH_HIBERNATION_POSSIBLE="y";
ARCH_KEEP_MEMBLOCK="y";
ARCH_MMAP_RND_BITS_MAX="15";
ARCH_MMAP_RND_COMPAT_BITS_MAX="15";
ARCH_SUSPEND_POSSIBLE="y";
AT803X_PHY="y";
BLK_MQ_PCI="y";
BOARD_SCACHE="y";
CEVT_R4K="y";
CLKSRC_MIPS_GIC="y";
CLK_MT7621="y";
CLOCKSOURCE_WATCHDOG="y";
CLONE_BACKWARDS="y";
CMDLINE_BOOL="y";
COMMON_CLK="y";
COMPAT_32BIT_TIME="y";
CPU_GENERIC_DUMP_TLB="y";
CPU_HAS_DIEI="y";
CPU_HAS_PREFETCH="y";
CPU_HAS_RIXI="y";
CPU_HAS_SYNC="y";
CPU_LITTLE_ENDIAN="y";
CPU_MIPS32="y";
CPU_MIPS32_R2="y";
CPU_MIPSR2="y";
CPU_MIPSR2_IRQ_EI="y";
CPU_MIPSR2_IRQ_VI="y";
CPU_NEEDS_NO_SMARTMIPS_OR_MICROMIPS="y";
CPU_R4K_CACHE_TLB="y";
CPU_RMAP="y";
CPU_SUPPORTS_32BIT_KERNEL="y";
CPU_SUPPORTS_HIGHMEM="y";
CPU_SUPPORTS_MSA="y";
CRC16="y";
CRYPTO_DEFLATE="y";
CRYPTO_HASH_INFO="y";
CRYPTO_LIB_BLAKE2S_GENERIC="y";
CRYPTO_LIB_POLY1305_RSIZE="2";
CRYPTO_LZO="y";
CRYPTO_ZSTD="y";
CSRC_R4K="y";
DIMLIB="y";
DMA_NONCOHERENT="y";
DTB_RT_NONE="y";
DTC="y";
EARLY_PRINTK="y";
FIXED_PHY="y";
FWNODE_MDIO="y";
FW_LOADER_PAGED_BUF="y";
GENERIC_ATOMIC64="y";
GENERIC_CLOCKEVENTS="y";
GENERIC_CMOS_UPDATE="y";
GENERIC_CPU_AUTOPROBE="y";
GENERIC_FIND_FIRST_BIT="y";
GENERIC_GETTIMEOFDAY="y";
GENERIC_IOMAP="y";
GENERIC_IRQ_CHIP="y";
GENERIC_IRQ_EFFECTIVE_AFF_MASK="y";
GENERIC_IRQ_SHOW="y";
GENERIC_LIB_ASHLDI3="y";
GENERIC_LIB_ASHRDI3="y";
GENERIC_LIB_CMPDI2="y";
GENERIC_LIB_LSHRDI3="y";
GENERIC_LIB_UCMPDI2="y";
GENERIC_PCI_IOMAP="y";
GENERIC_PHY="y";
GENERIC_PINCONF="y";
GENERIC_SCHED_CLOCK="y";
GENERIC_SMP_IDLE_THREAD="y";
GENERIC_TIME_VSYSCALL="y";
GLOB="y";
GPIOLIB_IRQCHIP="y";
GPIO_CDEV="y";
GPIO_GENERIC="y";
GPIO_MT7621="y";
GRO_CELLS="y";
HANDLE_DOMAIN_IRQ="y";
HARDWARE_WATCHPOINTS="y";
HAS_DMA="y";
HAS_IOMEM="y";
HAS_IOPORT_MAP="y";
I2C="y";
I2C_ALGOBIT="y";
I2C_BOARDINFO="y";
I2C_CHARDEV="y";
I2C_GPIO="y";
I2C_MT7621="y";
ICPLUS_PHY="y";
IRQCHIP="y";
IRQ_DOMAIN="y";
IRQ_DOMAIN_HIERARCHY="y";
IRQ_FORCED_THREADING="y";
IRQ_MIPS_CPU="y";
IRQ_WORK="y";
LIBFDT="y";
LOCK_DEBUGGING_SUPPORT="y";
LZO_COMPRESS="y";
LZO_DECOMPRESS="y";
MDIO_BUS="y";
MDIO_DEVICE="y";
MDIO_DEVRES="y";
MEDIATEK_GE_PHY="y";
MEMFD_CREATE="y";
MFD_SYSCON="y";
MIGRATION="y";
MIKROTIK="y";
MIKROTIK_RB_SYSFS="y";
MIPS="y";
MIPS_ASID_BITS="8";
MIPS_ASID_SHIFT="0";
MIPS_CLOCK_VSYSCALL="y";
MIPS_CM="y";
MIPS_CPC="y";
MIPS_CPS="y";
MIPS_CPU_SCACHE="y";
MIPS_GIC="y";
MIPS_L1_CACHE_SHIFT="5";
MIPS_LD_CAN_LINK_VDSO="y";
MIPS_MT="y";
MIPS_MT_FPAFF="y";
MIPS_MT_SMP="y";
MIPS_NR_CPU_NR_MAP="4";
MIPS_PERF_SHARED_TC_COUNTERS="y";
MIPS_SPRAM="y";
MODULES_USE_ELF_REL="y";
MTD_CMDLINE_PARTS="y";
MTD_NAND_CORE="y";
MTD_NAND_ECC="y";
MTD_NAND_ECC_SW_HAMMING="y";
MTD_NAND_MT7621="y";
MTD_NAND_MTK_BMT="y";
MTD_RAW_NAND="y";
MTD_ROUTERBOOT_PARTS="y";
MTD_SERCOMM_PARTS="y";
MTD_SPI_NOR="y";
MTD_SPLIT_FIT_FW="y";
MTD_SPLIT_MINOR_FW="y";
MTD_SPLIT_SEAMA_FW="y";
MTD_SPLIT_TPLINK_FW="y";
MTD_SPLIT_TRX_FW="y";
MTD_SPLIT_UIMAGE_FW="y";
MTD_UBI="y";
MTD_UBI_BEB_LIMIT="20";
MTD_UBI_BLOCK="y";
MTD_UBI_WL_THRESHOLD="4096";
MTD_VIRT_CONCAT="y";
NEED_DMA_MAP_STATE="y";
NET_DEVLINK="y";
NET_DSA="y";
NET_DSA_MT7530="y";
NET_DSA_MT7530_MDIO="y";
NET_DSA_TAG_MTK="y";
NET_FLOW_LIMIT="y";
NET_MEDIATEK_SOC="y";
NET_SELFTESTS="y";
NET_SWITCHDEV="y";
NET_VENDOR_MEDIATEK="y";
NO_HZ_COMMON="y";
NO_HZ_IDLE="y";
NR_CPUS="4";
NVMEM="y";
OF="y";
OF_ADDRESS="y";
OF_EARLY_FLATTREE="y";
OF_FLATTREE="y";
OF_GPIO="y";
OF_IRQ="y";
OF_KOBJ="y";
OF_MDIO="y";
PAGE_POOL="y";
PAGE_POOL_STATS="y";
PCI="y";
PCIE_MT7621="y";
PCI_DISABLE_COMMON_QUIRKS="y";
PCI_DOMAINS="y";
PCI_DOMAINS_GENERIC="y";
PCI_DRIVERS_GENERIC="y";
PCS_MTK_LYNXI="y";
PERF_USE_VMALLOC="y";
PGTABLE_LEVELS="2";
PHYLIB="y";
PHYLINK="y";
PHY_MT7621_PCI="y";
PINCTRL="y";
PINCTRL_AW9523="y";
PINCTRL_MT7621="y";
PINCTRL_RALINK="y";
PINCTRL_SX150X="y";
POWER_RESET="y";
POWER_RESET_GPIO="y";
POWER_SUPPLY="y";
PTP_1588_CLOCK_OPTIONAL="y";
QUEUED_RWLOCKS="y";
QUEUED_SPINLOCKS="y";
RALINK="y";
RATIONAL="y";
REGMAP="y";
REGMAP_I2C="y";
REGMAP_MMIO="y";
REGULATOR="y";
REGULATOR_FIXED_VOLTAGE="y";
RESET_CONTROLLER="y";
RFS_ACCEL="y";
RPS="y";
RTC_CLASS="y";
RTC_DRV_BQ32K="y";
RTC_DRV_PCF8563="y";
RTC_I2C_AND_SPI="y";
SCHED_SMT="y";
SERIAL_8250="y";
SERIAL_8250_CONSOLE="y";
SERIAL_8250_NR_UARTS="3";
SERIAL_8250_RUNTIME_UARTS="3";
SERIAL_MCTRL_GPIO="y";
SERIAL_OF_PLATFORM="y";
SGL_ALLOC="y";
SMP="y";
SMP_UP="y";
SOCK_RX_QUEUE_MAPPING="y";
SOC_BUS="y";
SOC_MT7621="y";
SPI="y";
SPI_MASTER="y";
SPI_MEM="y";
SPI_MT7621="y";
SRCU="y";
SWPHY="y";
SYNC_R4K="y";
SYSCTL_EXCEPTION_TRACE="y";
SYS_HAS_CPU_MIPS32_R1="y";
SYS_HAS_CPU_MIPS32_R2="y";
SYS_HAS_EARLY_PRINTK="y";
SYS_SUPPORTS_32BIT_KERNEL="y";
SYS_SUPPORTS_ARBIT_HZ="y";
SYS_SUPPORTS_HIGHMEM="y";
SYS_SUPPORTS_HOTPLUG_CPU="y";
SYS_SUPPORTS_LITTLE_ENDIAN="y";
SYS_SUPPORTS_MIPS16="y";
SYS_SUPPORTS_MIPS_CPS="y";
SYS_SUPPORTS_MULTITHREADING="y";
SYS_SUPPORTS_SCHED_SMT="y";
SYS_SUPPORTS_SMP="y";
SYS_SUPPORTS_ZBOOT="y";
TARGET_ISA_REV="2";
TICK_CPU_ACCOUNTING="y";
TIMER_OF="y";
TIMER_PROBE="y";
TREE_RCU="y";
TREE_SRCU="y";
UBIFS_FS="y";
USB_SUPPORT="y";
USE_OF="y";
WEAK_ORDERING="y";
XPS="y";
XXHASH="y";
ZLIB_DEFLATE="y";
ZLIB_INFLATE="y";
ZSTD_COMPRESS="y";
ZSTD_DECOMPRESS="y";
} // lib.optionalAttrs (config.system.service ? watchdog) {
RALINK_WDT = "y"; # watchdog
MT7621_WDT = "y"; # or it might be this one
};
conditionalConfig = {
WLAN = {
MT7915E = "m";
};
};
};
tplink-safeloader.board = "ARCHER-AX23-V1";
boot = {
commandLine = [ "console=ttyS0,115200" ];
tftp = {
# Should be a segment of free RAM, where the tftp artifact
# can be stored before unpacking it to the 'hardware.loadAddress'
# The 'hardware.loadAddress' is 0x80001000, which suggests the
# RAM would start at 0x8000000 and (being 128MB) go to
# to 0x8800000. Let's put it at the 100MB mark at
# 0x8000000+0x0640000=0x86400000
loadAddress = lim.parseInt "0x86400000";
};
};
filesystem =
let inherit (pkgs.pseudofile) dir symlink;
in
dir {
lib = dir {
firmware = dir {
mediatek = symlink firmware;
};
};
};
hardware =
let
openwrt = pkgs.openwrt;
mac80211 = pkgs.kmodloader.override {
targets = [
"mt7915e"
];
inherit (config.system.outputs) kernel;
};
in {
# from OEM bootlog (openwrt wiki):
# 4 cmdlinepart partitions found on MTD device raspi
# Creating 4 MTD partitions on "raspi":
# 0x000000000000-0x000000040000 : "uboot"
# 0x000000040000-0x000000440000 : "uImage"
# 0x000000440000-0x000000ff0000 : "rootfs"
# 0x000000ff0000-0x000001000000 : "ART"
# from openwrt bootlog (openwrt wiki):
# 5 fixed-partitions partitions found on MTD device spi0.0
# OF: Bad cell count for /palmbus@1e000000/spi@b00/flash@0/partitions
# OF: Bad cell count for /palmbus@1e000000/spi@b00/flash@0/partitions
# OF: Bad cell count for /palmbus@1e000000/spi@b00/flash@0/partitions
# OF: Bad cell count for /palmbus@1e000000/spi@b00/flash@0/partitions
# Creating 5 MTD partitions on "spi0.0":
# 0x000000000000-0x000000040000 : "u-boot"
# 0x000000040000-0x000000fa0000 : "firmware"
# 2 uimage-fw partitions found on MTD device firmware
# Creating 2 MTD partitions on "firmware":
# 0x000000000000-0x0000002c0000 : "kernel"
# 0x0000002c0000-0x000000f60000 : "rootfs"
# mtd: setting mtd3 (rootfs) as root device
# 1 squashfs-split partitions found on MTD device rootfs
# 0x000000640000-0x000000f60000 : "rootfs_data"
# 0x000000fa0000-0x000000fb0000 : "config"
# 0x000000fb0000-0x000000ff0000 : "tplink"
# 0x000000ff0000-0x000001000000 : "radio"
flash = {
# from the OEM bootlog 'Booting image at bc040000'
# (0x40000 from 0xbc000000)
address = lim.parseInt "0xbc040000";
# 0x000000040000-0x000000fa0000
size = lim.parseInt "0xf60000";
# TODO: find in /proc/mtd on a running system
eraseBlockSize = 65536;
};
# since this is mentioned in the partition table as well?
defaultOutput = "tplink-safeloader";
# taken from openwrt sysupgrade image:
# openwrt-23.05.2-ramips-mt7621-tplink_archer-ax23-v1-squashfs-sysupgrade.bin: u-boot legacy uImage, MIPS OpenWrt Linux-5.15.137, Linux/MIPS, OS Kernel Image (lzma), 2797386 bytes, Tue Nov 14 13:38:11 2023, Load Address: 0X80001000, Entry Point: 0X80001000, Header CRC: 0X19F74C5B, Data CRC: 0XF685563C
loadAddress = lim.parseInt "0x80001000";
entryPoint = lim.parseInt "0x80001000";
rootDevice = "/dev/mtdblock3";
dts = {
src = "${openwrt.src}/target/linux/ramips/dts/mt7621_tplink_archer-ax23-v1.dts";
includes = [
"${openwrt.src}/target/linux/ramips/dts"
"${config.system.outputs.kernel.modulesupport}/arch/arm64/boot/dts/mediatek/"
];
};
networkInterfaces =
let
inherit (config.system.service.network) link;
inherit (config.system.service) bridge;
in rec {
lan1 = link.build { ifname = "lan1"; };
lan2 = link.build { ifname = "lan2"; };
lan3 = link.build { ifname = "lan3"; };
lan4 = link.build { ifname = "lan4"; };
wan = link.build { ifname = "wan"; };
wlan = link.build {
ifname = "wlan0";
dependencies = [ mac80211 ];
};
wlan5 = link.build {
ifname = "wlan1";
dependencies = [ mac80211 ];
};
};
};
};
};
}

View File

@ -1,412 +0,0 @@
{
description = ''
Turris Omnia
************
This is a 32 bit ARMv7 MVEBU device, which is usually shipped with
TurrisOS, an OpenWrt-based system. Rather than reformatting the
builtin storage, we install Liminix on to the existing btrfs
filesystem so that the vendor snapshot/recovery system continues
to work (and provides you an easy rollback if you decide you don't
like Liminix after all).
The install process has two stages, and is intended that you
should not need to open the device and add a serial console
(although it may be handy for visibility, and in case anything
goes wrong). First we build a minimal installation/recovery
system, then we reboot into that recovery image to prepare the
device for the full target install.
Installation using a USB stick
==============================
First, build the image for the USB stick. Review
:file:`examples/recovery.nix` in order to change the default
root password (which is ``secret``) and/or the SSH keys, then
build it with
.. code-block:: console
$ nix-build -I liminix-config=./examples/recovery.nix \
--arg device "import ./devices/turris-omnia" \
-A outputs.mbrimage -o mbrimage
$ file -L mbrimage
mbrimage: DOS/MBR boot sector; partition 1 : ID=0x83, active, start-CHS (0x0,0,5), end-CHS (0x6,130,26), startsector 4, 104602 sectors
Next, copy the image from your build machine to a USB storage
medium using :command:`dd` or your other most favoured file copying
tool, which might be a comand something like this:
.. code-block:: console
$ dd if=mbrimage of=/dev/path/to/the/usb/stick \
bs=1M conv=fdatasync status=progress
The Omnia's default boot order only checks USB after it has failed
to boot from eMMC, which is not ideal for our purpose. Unless you
have a serial cable, the easiest way to change this is by booting
to TurrisOS and logging in with ssh:
.. code-block:: console
root@turris:/# fw_printenv boot_targets
boot_targets=mmc0 nvme0 scsi0 usb0 pxe dhcp
root@turris:/# fw_setenv boot_targets usb0 mmc0
root@turris:/# fw_printenv boot_targets
boot_targets=usb0 mmc0
root@turris:/# reboot -f
It should now boot into the recovery image. It expects a network
cable to be plugged into LAN2 with something on the other end of
it that serves DHCP requests. Check your DHCP server logs for a
request from a ``liminix-recovery`` host and figure out what IP
address was assigned.
.. code-block:: console
$ ssh liminix-recovery.lan
You should get a "Busybox" banner and a root prompt. Now you can
start preparing the device to install Liminix on it. First we'll
mount the root filesystem and take a snapshot:
.. code-block:: console
# mkdir /dest && mount /dev/mmcblk0p1 /dest
# schnapps -d /dest create "pre liminix"
# schnapps -d /dest list
ERROR: not a valid btrfs filesystem: /
# | Type | Size | Date | Description
------+-----------+-------------+---------------------------+------------------------------------
1 | single | 16.00KiB | 1970-01-01 00:11:49 +0000 | pre liminix
(``not a valid btrfs filesystem: /`` is not a real error)
then we can remove all the files
.. code-block:: console
# rm -r /dest/@/*
and then it's ready to install the real Liminix system onto. On
your build system, create the Liminix configuration you wish to
install: here we'll use the ``rotuer`` example.
.. code-block:: console
build$ nix-build -I liminix-config=./examples/rotuer.nix \
--arg device "import ./devices/turris-omnia" \
-A outputs.systemConfiguration
and then use :command:`min-copy-closure` to copy it to the device.
.. code-block:: console
build$ nix-shell --run \
"min-copy-closure -r /dest/@ root@liminix-recovery.lan result"
and activate it
.. code-block:: console
build$ ssh root@liminix-recovery.lan \
"/dest/@/$(readlink result)/bin/install /dest/@"
The final steps are performed directly on the device again: add
a symlink so U-Boot can find :file:`/boot`, then restore the
default boot order and reboot into the new configuration.
.. code-block:: console
# cd /dest && ln -s @/boot .
# fw_setenv boot_targets "mmc0 nvme0 scsi0 usb0 pxe dhcp"
# cd / ; umount /dest
# reboot
Installation using a TFTP server and serial console
===================================================
If you have a :ref:`serial` console connection and a TFTP server,
and would rather use them than fiddling with USB sticks, the
:file:`examples/recovery.nix` configuration also works
using the ``tftpboot`` output. So you can do
.. code-block:: console
build$ nix-build -I liminix-config=./examples/recovery.nix \
--arg device "import ./devices/turris-omnia" \
-A outputs.tftpboot
and then paste the generated :file:`result/boot.scr` into
U-Boot, and you will end up with the same system as you would
have had after booting from USB. If you don't have a serial
console connection you could probably even get clever with
elaborate use of :command:`fw_setenv`, but that is left as
an exercise for the reader.
'';
system = {
crossSystem = {
config = "armv7l-unknown-linux-musleabihf";
};
};
module = {pkgs, config, lib, lim, ... }:
let
openwrt = pkgs.openwrt;
inherit (lib) mkOption types;
inherit (pkgs.liminix.services) oneshot;
inherit (pkgs) liminix;
mtd_by_name_links = pkgs.liminix.services.oneshot rec {
name = "mtd_by_name_links";
up = ''
mkdir -p /dev/mtd/by-name
cd /dev/mtd/by-name
for i in /sys/class/mtd/mtd*[0-9]; do
ln -s ../../$(basename $i) $(cat $i/name)
done
'';
};
in {
imports = [
../../modules/arch/arm.nix
../../modules/outputs/tftpboot.nix
../../modules/outputs/mbrimage.nix
../../modules/outputs/extlinux.nix
];
config = {
services.mtd-name-links = mtd_by_name_links;
kernel = {
src = pkgs.pkgsBuildBuild.fetchurl {
name = "linux.tar.gz";
url = "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.7.4.tar.gz";
hash = "sha256-wIrmL0BS63nRwWfm4nw+dRNVPUzGh9M4X7LaHzAn5tU=";
};
version = "6.7.4";
config = {
PCI = "y";
OF = "y";
MEMORY = "y"; # for MVEBU_DEVBUS
DMADEVICES = "y"; # for MV_XOR
CPU_V7 = "y";
ARCH_MULTIPLATFORM = "y";
ARCH_MVEBU = "y";
ARCH_MULTI_V7= "y";
PCI_MVEBU = "y";
AHCI_MVEBU = "y";
RTC_CLASS = "y";
RTC_DRV_ARMADA38X = "y"; # this may be useful anyway?
EXPERT = "y";
ALLOW_DEV_COREDUMP = "n";
# dts has a compatible for this but dmesg is not
# showing it
EEPROM_AT24 = "y"; # atmel,24c64
I2C = "y";
I2C_MUX = "y";
I2C_MUX_PCA954x = "y";
MACH_ARMADA_38X = "y";
SMP = "y";
# this is disabled for the moment because it relies on a
# GCC plugin that requires gmp.h to build, and I can't see
# right now how to confgure it to find gmp
STACKPROTECTOR_PER_TASK = "n";
NR_CPUS = "4";
VFP = "y";
NEON= "y";
# WARNING: unmet direct dependencies detected for ARCH_WANT_LIBATA_LEDS
ATA = "y";
PSTORE = "y";
PSTORE_RAM = "y";
PSTORE_CONSOLE = "y";
# PSTORE_DEFLATE_COMPRESS = "n";
BLOCK = "y";
MMC="y";
PWRSEQ_EMMC="y"; # ???
PWRSEQ_SIMPLE="y"; # ???
MMC_BLOCK="y";
MMC_SDHCI= "y";
MMC_SDHCI_PLTFM= "y";
MMC_SDHCI_PXAV3= "y";
MMC_MVSDIO= "y";
SERIAL_8250 = "y";
SERIAL_8250_CONSOLE = "y";
SERIAL_OF_PLATFORM="y";
SERIAL_MVEBU_UART = "y";
SERIAL_MVEBU_CONSOLE = "y";
SERIAL_8250_DMA= "y";
SERIAL_8250_DW= "y";
SERIAL_8250_EXTENDED= "y";
SERIAL_8250_MANY_PORTS= "y";
SERIAL_8250_SHARE_IRQ= "y";
OF_ADDRESS= "y";
OF_MDIO= "y";
WATCHDOG = "y"; # watchdog is enabled by u-boot
ORION_WATCHDOG = "y"; # so is non-optional to keep feeding
MVEBU_DEVBUS = "y"; # "Device Bus controller ... flash devices such as NOR, NAND, SRAM, and FPGA"
MVMDIO = "y";
MVNETA = "y";
MVNETA_BM = "y";
MVNETA_BM_ENABLE = "y";
SRAM = "y"; # mmio-sram is "compatible" for bm_bppi reqd by BM
PHY_MVEBU_A38X_COMPHY = "y"; # for eth2
MARVELL_PHY = "y";
MVPP2 = "y";
MV_XOR = "y";
# there is NOR flash on this device, which is used for U-Boot
# and the rescue system (which we don't interfere with) but
# also for the U-Boot environment variables (which we might
# need to meddle with)
MTD_SPI_NOR = "y";
SPI = "y";
SPI_MASTER = "y";
SPI_ORION = "y";
NET_DSA = "y";
NET_DSA_MV88E6XXX = "y"; # depends on PTP_1588_CLOCK_OPTIONAL
};
conditionalConfig = {
USB = {
USB_XHCI_MVEBU = "y";
USB_XHCI_HCD = "y";
};
WLAN = {
WLAN_VENDOR_ATH = "y";
ATH_COMMON = "m";
ATH9K = "m";
ATH9K_PCI = "y";
ATH10K = "m";
ATH10K_PCI = "m";
ATH10K_DEBUG = "y";
};
};
};
boot = {
commandLine = [
"console=ttyS0,115200"
"pcie_aspm=off" # ath9k pci incompatible with PCIe ASPM
];
};
filesystem =
let
inherit (pkgs.pseudofile) dir symlink;
firmware = pkgs.stdenv.mkDerivation {
name = "wlan-firmware";
phases = ["installPhase"];
installPhase = ''
mkdir $out
cp -r ${pkgs.linux-firmware}/lib/firmware/ath10k/QCA988X $out
'';
};
in dir {
lib = dir {
firmware = dir {
ath10k = symlink firmware;
};
};
etc = dir {
"fw_env.config" =
let f = pkgs.writeText "fw_env.config" ''
/dev/mtd/by-name/u-boot-env 0x0 0x10000 0x10000
'';
in symlink f;
};
};
boot.tftp = {
loadAddress = lim.parseInt "0x1700000";
kernelFormat = "zimage";
compressRoot = true;
};
hardware = let
mac80211 = pkgs.kmodloader.override {
inherit (config.system.outputs) kernel;
targets = ["ath9k" "ath10k_pci"];
};
in {
defaultOutput = "mtdimage";
loadAddress = lim.parseInt "0x00800000"; # "0x00008000";
entryPoint = lim.parseInt "0x00800000"; # "0x00008000";
rootDevice = "/dev/mmcblk0p1";
dts = {
src = "${config.system.outputs.kernel.modulesupport}/arch/arm/boot/dts/marvell/armada-385-turris-omnia.dts";
includes = [
"${config.system.outputs.kernel.modulesupport}/arch/arm/boot/dts/marvell/"
];
};
flash.eraseBlockSize = 65536; # only used for tftpboot
networkInterfaces =
let
inherit (config.system.service.network) link;
inherit (config.system.service) bridge;
in rec {
en70000 = link.build {
# in armada-38x.dtsi this is eth0.
# It's connected to port 5 of the 88E6176 switch
devpath = "/devices/platform/soc/soc:internal-regs/f1070000.ethernet";
# name is unambiguous but not very semantic
ifname = "en70000";
};
en30000 = link.build {
# in armada-38x.dtsi this is eth1
# It's connected to port 6 of the 88E6176 switch
devpath = "/devices/platform/soc/soc:internal-regs/f1030000.ethernet";
# name is unambiguous but not very semantic
ifname = "en30000";
};
# the default (from the dts? I'm guessing) behavour for
# lan ports on the switch is to attach them to
# en30000. It should be possible to do something better,
# per
# https://www.kernel.org/doc/html/latest/networking/dsa/configuration.html#affinity-of-user-ports-to-cpu-ports
# but apparently OpenWrt doesn't either so maybe it's more
# complicated than it looks.
wan = link.build {
# in armada-38x.dtsi this is eth2. It may be connected to
# an ethernet phy or to the SFP cage, depending on a gpio
devpath = "/devices/platform/soc/soc:internal-regs/f1034000.ethernet";
ifname = "wan";
};
lan0 = link.build { ifname = "lan0"; };
lan1 = link.build { ifname = "lan1"; };
lan2 = link.build { ifname = "lan2"; };
lan3 = link.build { ifname = "lan3"; };
lan4 = link.build { ifname = "lan4"; };
lan5 = link.build { ifname = "lan5"; };
lan = lan0; # maybe we should build a bridge?
wlan = link.build {
ifname = "wlan0";
dependencies = [ mac80211 ];
};
wlan5 = link.build {
ifname = "wlan1";
dependencies = [ mac80211 ];
};
};
};
};
};
}

View File

@ -1,155 +0,0 @@
#include "mt7621.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/input/input.h>
/ {
aliases {
label-mac-device = &gmac0;
};
};
&nand {
status = "okay";
mediatek,nmbm;
mediatek,bmt-max-ratio = <15>;
mediatek,bmt-max-reserved-blocks = <64>;
mediatek,bmt-remap-range =
<0x0 0x980000>,
<0x2980000 0x7800000>;
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition@0 {
label = "u-boot";
reg = <0x0 0x80000>;
read-only;
};
partition@80000 {
label = "u-boot-env";
reg = <0x80000 0x80000>;
read-only;
};
factory: partition@100000 {
label = "factory";
reg = <0x100000 0x80000>;
read-only;
};
partition@180000 {
label = "firmware_a";
reg = <0x180000 0x2800000>;
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition@0 {
label = "kernel_a";
reg = <0x0 0x800000>;
};
partition@400000 {
label = "ubi";
reg = <0x800000 0x2000000>;
};
};
partition@2980000 {
label = "firmware_b";
reg = <0x2980000 0x2800000>;
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition@0 {
label = "kernel_b";
reg = <0x0 0x800000>;
};
partition@400000 {
label = "ubi_b";
reg = <0x800000 0x2000000>;
};
};
partition@5180000 {
label = "rootfs_data";
reg = <0x5180000 0x1400000>;
};
partition@6580000 {
label = "logs";
reg = <0x6580000 0xd00000>;
};
partition@7280000 {
label = "vendor-myzyxel";
reg = <0x7280000 0x480000>;
read-only;
};
partition@7700000 {
label = "bootconfig";
reg = <0x7700000 0x80000>;
};
mrd: partition@7780000 {
label = "mrd";
reg = <0x7780000 0x80000>;
read-only;
nvmem-layout {
compatible = "fixed-layout";
#address-cells = <1>;
#size-cells = <1>;
macaddr_mrd_1fff8: macaddr@1fff8 {
reg = <0x1fff8 0x6>;
};
};
};
};
};
&pcie {
status = "okay";
};
&pcie1 {
wlan_5g: wifi@0,0 {
reg = <0x0 0 0 0 0>;
compatible = "mediatek,mt76";
mediatek,mtd-eeprom = <&factory 0x0>;
/* MAC-Address set in userspace */
};
};
&gmac0 {
nvmem-cells = <&macaddr_mrd_1fff8>;
nvmem-cell-names = "mac-address";
};
&switch0 {
ports {
port@4 {
status = "okay";
label = "lan";
};
};
};
&state_default {
gpio {
groups = "uart3";
function = "gpio";
};
};

View File

@ -1,155 +0,0 @@
#include "mt7621.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/input/input.h>
/ {
aliases {
label-mac-device = &gmac0;
};
};
&nand {
status = "okay";
mediatek,nmbm;
mediatek,bmt-max-ratio = <15>;
mediatek,bmt-max-reserved-blocks = <64>;
mediatek,bmt-remap-range =
<0x0 0x980000>,
<0x2980000 0x7800000>;
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition@0 {
label = "u-boot";
reg = <0x0 0x80000>;
read-only;
};
partition@80000 {
label = "u-boot-env";
reg = <0x80000 0x80000>;
read-only;
};
factory: partition@100000 {
label = "factory";
reg = <0x100000 0x80000>;
read-only;
};
partition@2980000 {
label = "firmware_b";
reg = <0x2980000 0x2800000>;
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition@0 {
label = "kernel_b";
reg = <0x0 0x800000>;
};
partition@400000 {
label = "ubi";
reg = <0x800000 0x2000000>;
};
};
partition@180000 {
label = "firmware_a";
reg = <0x180000 0x2800000>;
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition@0 {
label = "kernel_a";
reg = <0x0 0x800000>;
};
partition@400000 {
label = "ubi_a";
reg = <0x800000 0x2000000>;
};
};
partition@5180000 {
label = "rootfs_data";
reg = <0x5180000 0x1400000>;
};
partition@6580000 {
label = "logs";
reg = <0x6580000 0xd00000>;
};
partition@7280000 {
label = "vendor-myzyxel";
reg = <0x7280000 0x480000>;
read-only;
};
partition@7700000 {
label = "bootconfig";
reg = <0x7700000 0x80000>;
};
mrd: partition@7780000 {
label = "mrd";
reg = <0x7780000 0x80000>;
read-only;
nvmem-layout {
compatible = "fixed-layout";
#address-cells = <1>;
#size-cells = <1>;
macaddr_mrd_1fff8: macaddr@1fff8 {
reg = <0x1fff8 0x6>;
};
};
};
};
};
&pcie {
status = "okay";
};
&pcie1 {
wlan_5g: wifi@0,0 {
reg = <0x0 0 0 0 0>;
compatible = "mediatek,mt76";
mediatek,mtd-eeprom = <&factory 0x0>;
/* MAC-Address set in userspace */
};
};
&gmac0 {
nvmem-cells = <&macaddr_mrd_1fff8>;
nvmem-cell-names = "mac-address";
};
&switch0 {
ports {
port@4 {
status = "okay";
label = "lan";
};
};
};
&state_default {
gpio {
groups = "uart3";
function = "gpio";
};
};

View File

@ -1,367 +0,0 @@
{
system = {
crossSystem = {
config = "mipsel-unknown-linux-musl";
gcc = {
abi = "32";
arch = "mips32"; # mips32r2?
};
};
};
description = ''
Zyxel NWA50AX
********************
Zyxel NWA50AX is quite close to the GL-MT300N-v2 "Mango" device, but it is based on the MT7621
chipset instead of the MT7628.
Installation
============
This device is pretty, but, due to its A/B capabilities, can be a bit hard
to use completely.
The stock vendor firmware is a downstream fork of U-Boot: <https://github.com/RaitoBezarius/uboot-nwa50ax>
with restricted boot commands. Fortunately, OpenWrt folks figured out trivial command injections,
so you can use most of the OpenWrt commands without trouble by just command injecting
atns, atna or atnf, e.g. atns "; $real_command".
From factory web UI, you can upload the result of the zyxel-nwa-fit output.
From another operating system, you need to `dumpimage -T flat_dt -p 0 $zyxel-nwa-fit -o firmware.bin`,
`flash_erase $(mtd partition of the target partition firmware or zy_firmware) 0 0`, then you complete by
`nandwrite -p $(mtd partition of the target partition firmware or zy_firmware) firmware.bin`.
How to put the firmware.bin on the machine is left to you as an exercise, e.g. SSH, TFTP, whatever.
From serial, you have two choices:
- Flash this system via U-Boot:
same reasoning as from an existing Linux system, two choices:
- ymodem the binary, perform the write manually, you can inspire yourself
from the `script` contained in the vendor firmware, those are just a FIT containing a script.
- prepare a FIT containing a script executing your commands, tftpboot this.
- boot from an existing Liminix system, e.g. TFTPBOOT image.
- boot from an OpenWrt system, i.e. follow OpenWrt steps.
Once you are in a Linux system, understand that this device has A/B boot.
OpenWrt provides you with `zyxel-bootconfig` to set/unset the image status and choice.
The kernel is booted with `bootImage=<number>` which tells you which slot are you on.
You should find yourself with 10ish MTD partitions, the most interesting ones are two:
- firmware: 40MB
- firmware_1: 40MB
In the current setup, they are split further into kernel (8MB) and ubi (32MB).
Once you are done with first installation, note that if you want to use the A/B feature,
you need to write a _secondary_ image on the slot B. There is no proper flashing code
that will set the being-updated slot to `new` and boot on it to verify if it's working.
This is a WIP.
Upgrading your system can be achieved via:
- `liminix-rebuild` for the userspace.
- `flash_erase` + `nandwrite` for the kernelspace to the other slot than the one you are booted on,
note that you can just nandwrite the mtd partition corresponding to the *kernel* and not the whole firmware.
If you soft-bricked your AP, i.e. you cannot boot anything in U-Boot, no worries, just plug the serial console,
prepare a TFTP server (via `tufted` for example), download vendor firmware, set up `atns`, `atnf`, etc. and run `atnz`.
This will reflash everything back to normal via TFTP.
If you hard-bricked your AP, i.e. U-Boot is telling you to transfer a valid bootloader via ymodem, just extract
a U-Boot from the vendor OS, send it via ymodem and use the previous operations to perform a full flash this time
of all partitions.
Note that if you erased your MRD partition, you lost your serial and MAC address. There's no way to recover the original one
except by reading the physical label on your device!
If you super-hard-bricked your AP, i.e. no output on serial console, congratulations, you reached one of the rare state
of this device. You need an external NAND flasher to repair it and write the first stage from Mediatek to continue the previous
recovery operations.
Development TODO list:
- Better support for upgrade automation w.r.t. to A/B, e.g. automagic scripts.
- Mount the logs partition, mount / as overlayfs of firmware ? rootfs and rootfs_data for extended data.
- Jitter-based entropy injection? Device can be slow to initialize its CRNG and hostapd will reject few clients at the start because of that.
- Defaults for hostapd based on MT7915 capabilities? See the example for one possible list.
- Remove primary/secondary hack and put it in preinit.
- Offer ways to reflash the *bootloader* itself to support direct boot via UBI and kernel upgrades via filesystem rewrite.
Vendor web page: https://www.zyxel.com/fr/fr/products/wireless/ax1800-wifi-6-dual-radio-nebulaflex-access-point-nwa50ax
OpenWrt web page: https://openwrt.org/inbox/toh/zyxel/nwa50ax
OpenWrt tech data: https://openwrt.org/toh/hwdata/zyxel/zyxel_nwa50ax
'';
module = { pkgs, config, lib, lim, ...}:
let
inherit (pkgs.liminix.networking) interface;
inherit (pkgs.liminix.services) oneshot;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) openwrt;
mac80211 = pkgs.mac80211.override {
drivers = [ "mt7915e" ];
klibBuild = config.system.outputs.kernel.modulesupport;
};
# v204520220929
wlan_firmware = pkgs.fetchurl {
url = "https://github.com/openwrt/mt76/raw/1b88dd07f153b202e57fe29734806744ed006b0e/firmware/mt7915_wa.bin";
hash = "sha256-wooyefzb0i8640+lwq3vNhcBXRFCtGuo+jiL7afZaKA=";
};
wlan_firmware' = pkgs.fetchurl {
url = "https://github.com/openwrt/mt76/raw/1b88dd07f153b202e57fe29734806744ed006b0e/firmware/mt7915_wm.bin";
hash = "sha256-k62nQewRuKjBLd5R3RxU4F74YKnQx5zr6gqMMImqVQw=";
};
wlan_firmware'' = pkgs.fetchurl {
url = "https://github.com/openwrt/mt76/raw/1b88dd07f153b202e57fe29734806744ed006b0e/firmware/mt7915_rom_patch.bin";
hash = "sha256-ifriAjWzFACrxVWCANZpUaEZgB/0pdbhnTVQytx6ddg=";
};
in {
imports = [
# We include it to ensure the bridge functionality
# is available on the target kernel.
../../modules/bridge
../../modules/arch/mipsel.nix
../../modules/outputs/tftpboot.nix
../../modules/outputs/zyxel-nwa-fit.nix
../../modules/zyxel-dual-image
];
filesystem = dir {
lib = dir {
firmware = dir {
mediatek = dir {
"mt7915_wa.bin" = symlink wlan_firmware;
"mt7915_wm.bin" = symlink wlan_firmware';
"mt7915_rom_patch.bin" = symlink wlan_firmware'';
};
};
};
};
rootfsType = "ubifs";
hardware = {
# Taken from OpenWRT
# root@OpenWrt:/# ubinfo /dev/ubi0
# ubi0
# Volumes count: 2
# Logical eraseblock size: 126976 bytes, 124.0 KiB
# Total amount of logical eraseblocks: 256 (32505856 bytes, 31.0 MiB)
# Amount of available logical eraseblocks: 0 (0 bytes)
# Maximum count of volumes 128
# Count of bad physical eraseblocks: 0
# Count of reserved physical eraseblocks: 19
# Current maximum erase counter value: 2
# Minimum input/output unit size: 2048 bytes
# Character device major/minor: 250:0
# Present volumes: 0, 1
ubi = {
minIOSize = "2048";
logicalEraseBlockSize = "126976";
physicalEraseBlockSize = "128KiB";
maxLEBcount = "256";
};
# This is a FIT containing a kernel padded and
# a UBI volume rootfs.
defaultOutput = "zyxel-nwa-fit";
loadAddress = lim.parseInt "0x80001000";
entryPoint = lim.parseInt "0x80001000";
# Aligned on 2kb.
alignment = 2048;
rootDevice = "ubi:rootfs";
dts = {
# Actually, this is not what we want.
# This DTS is insufficient.
src = ./mt7621_zyxel_nwa50ax.dtsi;
includes = [
# Here's one weird trick to make `ubi` detection
# out of the box.
# We will write ubi on /dev/firmware_a:rootfs location
# and same for /dev/firmware_b:rootfs.
# How do we distinguish both?
# We can just use the DTS to point ubi at A or B.
# This, unfortunately, means that we have "two images".
# But they are really just 1 image with 2 different DTS.
# TODO: improve this hack in preinit?
(if config.boot.imageType == "primary" then "${./a_image}" else "${./b_image}")
"${openwrt.src}/target/linux/ramips/dts"
];
};
networkInterfaces =
let
inherit (config.system.service.network) link;
in {
eth = link.build { ifname = "eth0"; };
lan = link.build { ifname = "lan"; };
wlan0 = link.build {
ifname = "wlan0";
dependencies = [ mac80211 ];
};
wlan1 = link.build {
ifname = "wlan1";
dependencies = [ mac80211 ];
};
};
};
boot = {
# Critical because NWA50AX will extend your cmdline with the image number booted.
# and some bootloader version.
# You don't want to find yourself being overridden.
commandLineDtbNode = "bootargs-override";
imageFormat = "fit";
tftp = {
# 5MB is nice.
freeSpaceBytes = 5 * 1024 * 1024;
loadAddress = lim.parseInt "0x2000000";
};
};
# Dual image management service in userspace.
services.zyxel-dual-image = config.boot.zyxel-dual-image.build {
ensureActiveImage = "primary";
# TODO: use mtd names rather…
# primary and secondary are always /dev/mtd3 by virtue of the
# dtb being not too wrong…
# TODO: remove this hack.
primaryMtdPartition = "/dev/mtd3";
secondaryMtdPartition = "/dev/mtd3";
bootConfigurationMtdPartition = "/dev/mtd12";
};
# DEVICE_VENDOR := ZyXEL
# KERNEL_SIZE := 8192k
# DEVICE_PACKAGES := kmod-mt7915-firmware zyxel-bootconfig
# KERNEL := kernel-bin | lzma | fit lzma $$(KDIR)/image-$$(firstword $$(DEVICE_DTS)).dtb
# IMAGES += factory.bin ramboot-factory.bin
# IMAGE/factory.bin := append-kernel | pad-to $$(KERNEL_SIZE) | append-ubi | zyxel-nwa-fit
# IMAGE/ramboot-factory.bin := append-kernel | pad-to $$(KERNEL_SIZE) | append-ubi
kernel = {
src = pkgs.fetchurl {
name = "linux.tar.gz";
url = "https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.137.tar.gz";
hash = "sha256-PkdzUKZ0IpBiWe/RS70J76JKnBFzRblWcKlaIFNxnHQ=";
};
extraPatchPhase = ''
${openwrt.applyPatches.ramips}
'';
config = {
RALINK = "y";
PCI = "y";
PHY_MT7621_PCI = "y";
PCIE_MT7621 = "y";
SOC_MT7621 = "y";
CLK_MT7621 = "y";
CLOCKSOURCE_WATCHDOG = "y";
SERIAL_8250_CONSOLE = "y";
SERIAL_8250 = "y";
SERIAL_CORE_CONSOLE = "y";
SERIAL_OF_PLATFORM = "y";
SERIAL_8250_NR_UARTS = "3";
SERIAL_8250_RUNTIME_UARTS = "3";
SERIAL_MCTRL_GPIO = "y";
CONSOLE_LOGLEVEL_DEFAULT = "8";
CONSOLE_LOGLEVEL_QUIET = "4";
# MTD_UBI_BEB_LIMIT = "20";
# MTD_UBI_WL_THRESHOLD = "4096";
MTD = "y";
MTD_BLOCK = "y"; # fix undefined ref to register_mtd_blktrans_dev
MTD_RAW_NAND = "y";
MTD_NAND_MT7621 = "y";
MTD_NAND_MTK_BMT = "y"; # Bad-block Management Table
MTD_NAND_ECC_SW_HAMMING= "y";
MTD_SPI_NAND= "y";
MTD_OF_PARTS = "y";
MTD_NAND_CORE= "y";
MTD_SPLIT_FIRMWARE= "y";
MTD_SPLIT_FIT_FW= "y";
PINCTRL = "y";
PINCTRL_MT7621 = "y";
I2C = "y";
I2C_MT7621 = "y";
SPI = "y";
MTD_SPI_NOR = "y";
SPI_MT7621 = "y";
SPI_MASTER = "y";
SPI_MEM = "y";
REGULATOR = "y";
REGULATOR_FIXED_VOLTAGE = "y";
RESET_CONTROLLER = "y";
POWER_RESET = "y";
POWER_RESET_GPIO = "y";
POWER_SUPPLY = "y";
LED_TRIGGER_PHY = "y";
PCI_DISABLE_COMMON_QUIRKS = "y";
PCI_DOMAINS = "y";
PCI_DOMAINS_GENERIC = "y";
PCI_DRIVERS_GENERIC = "y";
PCS_MTK_LYNXI = "y";
SOC_BUS = "y";
NET = "y";
ETHERNET = "y";
WLAN = "y";
PHYLIB = "y";
AT803X_PHY = "y";
FIXED_PHY = "y";
GENERIC_PHY = "y";
NET_DSA = "y";
NET_DSA_MT7530 = "y";
NET_DSA_MT7530_MDIO = "y";
NET_DSA_TAG_MTK = "y";
NET_MEDIATEK_SOC = "y";
NET_SWITCHDEV = "y";
NET_VENDOR_MEDIATEK = "y";
SWPHY = "y";
GPIOLIB = "y";
GPIO_MT7621 = "y";
OF_GPIO = "y";
EARLY_PRINTK = "y";
NEW_LEDS = "y";
LEDS_TRIGGERS = "y";
LEDS_CLASS = "y"; # required by rt2x00lib
LEDS_CLASS_MULTICOLOR = "y";
LEDS_BRIGHTNESS_HW_CHANGED = "y";
PRINTK_TIME = "y";
} // lib.optionalAttrs (config.system.service ? vlan) {
SWCONFIG = "y";
} // lib.optionalAttrs (config.system.service ? watchdog) {
RALINK_WDT = "y"; # watchdog
MT7621_WDT = "y"; # or it might be this one
};
};
};
}

View File

@ -1,56 +0,0 @@
#include "mt7621_zyxel_nwa-ax-for-ab.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/input/input.h>
/ {
compatible = "zyxel,nwa50ax", "mediatek,mt7621-soc";
model = "ZyXEL NWA50AX";
aliases {
led-boot = &led_system_green;
led-failsafe = &led_system_red;
led-running = &led_system_green;
led-upgrade = &led_system_red;
};
leds {
compatible = "gpio-leds";
led_system_red: system_red {
label = "red:system";
gpios = <&gpio 6 GPIO_ACTIVE_HIGH>;
};
led_system_green: system_green {
label = "green:system";
gpios = <&gpio 7 GPIO_ACTIVE_HIGH>;
};
system_blue {
label = "blue:system";
gpios = <&gpio 8 GPIO_ACTIVE_HIGH>;
};
};
keys {
compatible = "gpio-keys";
reset {
label = "reset";
gpios = <&gpio 30 GPIO_ACTIVE_LOW>;
linux,code = <KEY_RESTART>;
};
};
};
&ethernet {
pinctrl-0 = <&mdio_pins>, <&rgmii1_pins>;
};
&state_default {
gpio {
groups = "uart3", "rgmii2";
function = "gpio";
};
};

View File

@ -12,13 +12,9 @@ BUILDDIR = _build
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
hardware.rst: hardware.nix
@rm -f hardware.rst || true
@cp $$(nix-build hardware.nix) hardware.rst
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
html: Makefile
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@ -1,198 +0,0 @@
System Administration
#####################
Services on a running system
****************************
* add an s6-rc cheatsheet here
Flashing and updating
*********************
Flashing from Liminix
=====================
The flash procedure from an existing Liminix-system has two steps.
First we reboot the device (using "kexec") into an "ephemeral"
RAM-based version of the new configuration, then when we're happy it
works we can flash the image - and if it doesn't work we can reboot
the device again and it will boot from the old image.
Building the RAM-based image
----------------------------
To create the ephemeral image, build ``outputs.kexecboot`` instead of
``outputs.default``. This generates a directory containing the root
filesystem image and kernel, along with an executable called `kexec`
and a `boot.sh` script that runs it with appropriate arguments.
For example
.. code-block:: console
nix-build -I liminix-config=./examples/arhcive.nix \
--arg device "import ./devices/gl-ar750"
-A outputs.kexecboot && \
(tar chf - result | ssh root@the-device tar -C /run -xvf -)
and then login to the device and run
.. code-block:: console
cd /run/result
sh ./boot.sh .
This will load the new kernel and map the root filesystem into a RAM
disk, then start executing the new kernel. *This is effectively a
reboot - be sure to close all open files and finish anything else
you were doing first.*
If the new system crashes or is rebooted, then the device will revert
to the old configuration it finds in flash.
Building the second (permanent) image
-------------------------------------
While running in the kexecboot system, you can build the permanent
image and copy it to the device with :command:`ssh`
.. code-block:: console
build-machine$ nix-build -I liminix-config=./examples/arhcive.nix \
--arg device "import ./devices/gl-ar750"
-A outputs.default && \
(tar chf - result | ssh root@the-device tar -C /run -xvf -)
build-machine$ tar chf - result/firmware.bin | \
ssh root@the-device tar -C /run -xvf -
Next you need to 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"
Now run (in this example)
.. code-block:: console
flashcp -v firmware.bin /dev/mtd3
"I know my new image is good, can I skip the intermediate step?"
----------------------------------------------------------------
In addition to giving you a chance to see if the new image works, this
two-step process ensures that you're not copying the new image over
the top of the active root filesystem. Sometimes it works, but you
will at least need physical access to the device to power-cycle it
because it will be effectively frozen afterwards.
Flashing from the boot monitor
==============================
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 sometimes involves soldering: please refer
to :ref:`serial`.
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. After transferring the image onto the
device using e.g. :command:`ssh`, you can run it 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.
Updating an installed system (JFFS2)
************************************
Adding packages
===============
If your device is running a JFFS2 root filesystem, you can build
extra packages for it on your build system and copy them to the
device: any package in Nixpkgs or in the Liminix overlay is available
with the ``pkgs`` prefix:
.. 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 to the device: it doesn't update
any profile to add it to ``$PATH``
Rebuilding the system
=====================
:command:`liminix-rebuild` is the Liminix analogue of :command:`nixos-rebuild`, although its operation is a bit different because it expects to run on a build machine and then copy to the host device. Run it with the same ``liminix-config`` and ``device`` parameters as you would run :command:`nix-build`, and it will build any new/changed packages and then copy them to the device using SSH. For example:
.. code-block:: console
liminix-rebuild root@the-device -I liminix-config=./examples/rotuer.nix --arg device "import ./devices/gl-ar750"
This will
* build anything that needs building
* copy new or changed packages to the device
* reboot the device
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 a lot of things have changed (e.g. a new version of
nixpkgs).
* it cannot upgrade the kernel, only userland

View File

@ -1,6 +0,0 @@
Architecture Decision Records
#############################
In this directory you will find descriptions of Liminix architecture
decisions.

View File

@ -1,201 +0,0 @@
Module system
#############
**Status:** Adopted; implemented in July-September 2023
Context
*******
Liminix users need a way to assemble a full system configuration by
combining smaller, more isolated and reusable components, otherwise
systems will be unwieldy and copy-and-paste will be rife.
Alternatives
************
NixOS module system
===================
The NixOS module system addresses many of these concerns. A module is
a Nix function which accepts a ``configuration`` attrset and some
other parameters, and returns a new fragment of ``configuration``
which is merged into it. It includes a DSL describing the permitted
types of values for each key in the configuration, which is used for
checking that the supplied parameters are valid and also governs what
to do if two modules both specify a value for the same key. (Usually
they are "merged", using some type-appropriate concept of merging.)
Usually a NixOS module looks only (or mostly only) at a particular
subtree of the overall configuration which is hardcoded in the module
definition, but the configuration fragment it returns may touch any
part of the schema. For example, the factorio module refers to
``config.services.factorio``, and it returns values for keys in
``systemd.services.factorio`` and ``networking.firewall``. There is no
way to use this module to run **two** factorio services with different
config (e.g. on different ports) - the only way to make that
possible would be to extend the module definition so that it
accepts a collection of game configurations and then create
a systemd service for each.
NixWRT module system
====================
NixWRT, the (now defunct) predecessor of Liminix, used a homegrown
module system modelled on the Nixpkgs overlay pattern. Each module is
a function that accepts ``super`` and ``self`` parameters, and
using <handwaves>that fixpoint magic thing</handwaves>
is called in a chain with the configuration returned by the previous
module and the final configuration.
NixWRT modules mostly don't refer to the configuration object to
decide how to configure themselves, but accept their parameters
directly as function parameters. For example, the configuration
file for "arhcive" (a backup server) includes this text:
.. code-block:: nix
(sshd {
hostkey = secrets.sshHostKey;
authkeys = { root = lib.splitString "\n" secrets.myKeys; };
})
busybox
(usbdisk {
label = "backup-disk";
mountpoint = "/srv";
fstype = "ext4";
options = "rw";
})
This gives us flexibility that NixOS modules don't: for example, if we
want to mount two USB disks, we can simply repeat that module twice
with different parameters - and the module definition doesn't have to
handle it specially.
However, the downside of this system is that we didn't implement any
concept of "types" - there is no type information, so there is no
systematic checking that parameters are valid, and if two modules set
the same config key then the rules for merging are entirely ad hoc.
There is a further (arguable) downside, which is that the
configuration is not just data - it's now part code. While it could be
feasible (though I've never seen it done) to encode a NixOS
configuration using Yaml or XML and then manipulate it as data, this
is not even possible using the NixWRT system.
Use services for everything
===========================
The most common properties that a Liminix configuration needs to
define are:
* which services (processes) to run
* what packages to install
* permitted users and groups
* Linux kernel configuration options
* Busybox applets
* filesystem layout
Suppose we only had services?
A Liminix service is (also) a derivation, so it is able to
create any files it likes inside its own store path, and
transitively require other packages simply by referring to them.
If it needs particular kernel options it could define them
as kernel modules to be loaded on demand when the service
starts (see the nftables module for an example). However:
* there is no way for a service to add busybox modules
* it cannot create files outside of its store path, so
wouldn't be able to make e.g. :file:`/etc/something.conf`
* no way to create users/groups. We could steal the DynamicUsers idea
from systemd and make them on demand, but this starts to get a bit
more complicated.
These limitations force us to reject this option as a general
solution - though we should strive *where possible* to implement
functionality as services and to minimise the proportion of Liminix
that manipulates the global configuration.
Decision
********
"Why not both?" None of these options is sufficient alone, so we are
going to do a mixture.
We will use the NixOS module system, but instead of expecting modules
to create systemd services as instances, they will expose "service
templates": functions that accept an attrset and return an
appropriately configured service that can be assigned by the caller
to a key in ``config.services``.
We will typecheck the service template function parameters using the
same type-checking code as NixOS uses for its modules.
An example may make this clearer: 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 the actual service using the template.
Consequences
************
This decision has both good and bad consequences
Pro
===
* We have a workable system for reusing configuration elements in
Liminix.
* We have type checking for most imortant things, reducing the risk of
deploying an invalid configuration.
* We have a simple mechanism for creating multiple services based on
the same module, without buulding that logic into the module
definition itself. For example, we could create two SSH daemons on
different ports, or DHCP clients with different configurations on
different network devices.
* We expect to be able to automate the generation of module
documentation.
Con
===
* By departing somewhat from the NixOS conventions we increase the
amount of code we have to write/maintain ourselves - and the
learning burden on users who are already familiar with that system.
* Liminix configurations contain function calls and aren't just data,
which means we can ony realistically interpret or introspect
them with the Nix interpreter itself - we can't query them
as data with other non-Nix tools.

View File

@ -13,10 +13,7 @@ author = 'Daniel Barlow'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'sphinx.ext.autosectionlabel'
]
autosectionlabel_prefix_document = True
extensions = []
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
@ -28,11 +25,3 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
html_theme = 'alabaster'
html_static_path = ['_static']
html_theme_options = {
'logo': '/logo.svg',
'globaltoc_collapse': 'false',
'page_width': '90%',
'body_max_width': '90%',
'description': 'A Nix-based OpenWrt-style embedded Linux system for consumer wifi routers'
}

View File

@ -1,259 +0,0 @@
Configuration
#############
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` or :command:`liminix-rebuild` 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.
Configuration taxonomy
**********************
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.
Services
********
We use the `s6-rc service manager <https://www.skarnet.org/software/s6-rc/overview.html>`_ to start/stop/restart services and handle
service dependencies. Any attribute in `config.services` will become
part of the default set of services that s6-rc will try to bring up on
boot.
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.
..
TODO: explain service outputs
..
TODO: outputs that change, and services that poll other services
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 = liminix.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.

View File

@ -1,14 +1,14 @@
Development
###########
Developer Manual
################
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
than by building and flashing a new image every time. This manual
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`.
be added to your environment by running :command:`nix-shell`
@ -27,18 +27,19 @@ To build it,
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
In a ``buildEnv`` nix-shell, you can use the :command:`mips-vm` command
to run Qemu with appropriate options. 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.
.. code-block:: console
nix-shell --run "mips-vm result/vmlinux result/squashfs"
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:
that directory for console and monitor. Use :command:`connect-vm`
(also in the ``buildEnv`` environment) to connect to either of these
sockets, and ^O to disconnect.
Networking
==========
@ -52,11 +53,9 @@ the right way:
* 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:
A VM started with :command:`mips-vm` 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
----------------------
@ -88,70 +87,6 @@ time with configurations for RP-PPPoE and/or Accel PPP.`
Hardware devices
****************
.. _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 is device-specific. Usually it 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
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 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 large chunks of text into the
terminal emulator and have it work just like 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`.
To do anything useful with U-Boot you will probably need a way to get
large binary files onto the device, and the usual way to do this is by
adding a network connection and using TFTP to download them. It's
quite common that the device's U-Boot doesn't speak DHCP so it will
need a static LAN address. You might also want to keep it away from
your "real" LAN: see :ref:`bng` for some potentially useful tooling
to use it on an isolated network.
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
@ -180,7 +115,7 @@ Now add the device and server IP addresses to your configuration:
};
and then build the derivation for ``outputs.default`` or
``outputs.mtdimage`` (for which it will be an alias on any device
``outputs.flashimage`` (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
@ -213,8 +148,6 @@ U-Boot to transfer the kernel and filesystem over TFTP and boot the
kernel from RAM.
.. _bng:
Networking
==========

51
doc/etc.rst Normal file
View File

@ -0,0 +1,51 @@
The Future
##########
What about NixWRT?
This is an in-progress rewrite of NixWRT, incorporating Lessons
Learned. That said, as of today it is not yet at feature parity.
Liminix will eventually provide these differentiators over NixWRT:
* a writable filesystem so that software updates or reconfiguration
(e.g. changing passwords) don't require taking the device offline to
reflash it.
* more flexible service management with dependencies, to allow
configurations such as "route through PPPoE if it is healthy, with
fallback to LTE"
* a spec for valid configuration options (a la NixOS module options)
to that we can detect errors at evaluation time instead of producing
a bad image.
* a network-based mechanism for secrets management so that changes can
be pushed from a central location to several Liminix devices at once
* send device metrics and logs to a monitoring/alerting/o11y
infrastructure
Today though, it does approximately none of these things and certainly
not on real hardware.
Articles of interest
####################
* `Build Safety of Software in 28 Popular Home Routers <https://cyber-itl.org/assets/papers/2018/build_safety_of_software_in_28_popular_home_routers.pdf>`_: "of the access
points and routers we reviewed, not a single one took full
advantage of the basic application armoring features provided by
the operating system. Indeed, only one or two models even came
close, and no brand did well consistently across all models tested"
* `A PPPoE Implementation for Linux <https://static.usenix.org/publications/library/proceedings/als00/2000papers/papers/full_papers/skoll/skoll_html/index.html>`_:
"Many DSL service providers use PPPoE for residential broadband
Internet access. This paper briefly describes the PPPoE protocol,
presents strategies for implementing it under Linux and describes in
detail a user-space implementation of a PPPoE client."
* `PPP IPV6CP vs DHCPv6 at AAISP <https://www.revk.uk/2011/01/ppp-ipv6cp-vs-dhcpv6.html>`_
* `Creating a Home IPv6 Network (James Bottomley) <https://blog.hansenpartnership.com/creating-a-home-ipv6-network/>`_

View File

@ -1,10 +1,30 @@
{ eval, lib, pkgs }:
let
overlay = import ../overlay.nix;
pkgs = import <nixpkgs> ( {
overlays = [overlay];
config = {
allowUnsupportedSystem = true; # mipsel
permittedInsecurePackages = [
"python-2.7.18.6" # kernel backports needs python <3
];
};
});
inherit (pkgs) lib;
inherit (lib) types;
modulenames =
builtins.attrNames
(lib.filterAttrsRecursive
(n: t:
(t=="directory") ||
((t=="regular") && ((builtins.match ".*\\.nix$" n) != null)))
(builtins.readDir ../modules));
modulefiles = builtins.map (n: builtins.toPath "${../modules}/${n}") modulenames;
eval = (lib.evalModules {
modules = [
{ _module.args = { inherit pkgs; lib = pkgs.lib; }; }
] ++ modulefiles;
});
conf = eval.config;
rootDir = builtins.toPath ./..;
stripAnyPrefixes = lib.flip (lib.fold lib.removePrefix)
["${rootDir}/"];
optToDoc = name: opt : {
inherit name;
description = opt.description or null;
@ -21,12 +41,16 @@ let
then
let sd = lib.attrByPath item.loc ["not found"] conf;
in item // {
declarations = map stripAnyPrefixes item.declarations;
parameters =
let x = lib.mapAttrsToList optToDoc sd.parameters; in x;
}
else
item // { declarations = map stripAnyPrefixes item.declarations; };
in
builtins.map spliceServiceDefn
(pkgs.lib.optionAttrSetToDocList eval.options)
item;
o = builtins.map spliceServiceDefn
(pkgs.lib.optionAttrSetToDocList eval.options);
in {
doc = pkgs.writeText "options.yaml" ''
# ${./..}
${builtins.toJSON o}
'';
}

View File

@ -1,38 +0,0 @@
with import <nixpkgs> {} ;
let
inherit (builtins) stringLength readDir filter;
devices = filter (n: n != "families")
(lib.mapAttrsToList (n: t: n) (readDir ../devices));
texts = map (n:
let d = import ../devices/${n}/default.nix;
d' = {
description = "${n}\n${substring 0 (stringLength n) "********************************"}\n";
} // d;
installer =
if d ? description && d ? installer
then ''
The default installation route for this device is
:ref:`system-outputs-${d.installer}`
''
else "";
in d'.description)
devices;
in
writeText "hwdoc" ''
Supported hardware
##################
For development, the `GL.iNet GL-MT300A <https://www.gl-inet.com/products/gl-mt300a/>`_
is an attractive choice as it 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.
For a more powerful device, something with an ath10k would be the safe bet,
or the Linksys E8450 which seems popular in the openwrt community.
${lib.concatStringsSep "\n\n" texts}
''

View File

@ -6,13 +6,9 @@ Liminix
:caption: Contents:
intro
tutorial
configuration
admin
development
modules
hardware
outputs
user
developer
etc
Indices and tables

View File

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

View File

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

View File

@ -1,19 +0,0 @@
(local yaml (require :lyaml))
;; (local { : view } (require :fennel))
(fn output? [option]
(match option.loc
["system" "outputs" & _] true
_ false))
(fn sorted-options [options]
(table.sort
options
(fn [a b] (< a.name b.name)))
options)
(each [_ option (ipairs (sorted-options (yaml.load (io.read "*a"))))]
(when (and (output? option) (not option.internal))
(print (.. ".. _" (string.gsub option.name "%." "-") ":") "\n")
(print option.description)))

View File

@ -2,116 +2,68 @@
(local { : view } (require :fennel))
(fn basename [str ext]
(-> str
(string.gsub "(.*/)(.*)" "%2")
(string.gsub (.. ext "$") "")))
(fn headline [name]
(let [title (assert (basename name ".nix"))
len (title:len)]
(.. title "\n" (string.rep "=" len))))
(let [(_ _ basename) (string.find name ".*/([^/].*)")
len (basename:len)]
(print basename)
(print (string.rep "=" len))))
(fn read-preamble [pathname]
(let [pathname (if (string.match pathname ".nix$")
pathname
(.. pathname "/default.nix"))]
(with-open [f (assert (io.open pathname :r))]
(accumulate [lines nil
l (f:lines)
:until (not (= (string.sub l 1 2) "##"))]
(.. (or lines "") (string.gsub l "^## *" "") "\n")))))
(fn relative-pathname [pathname]
(let [pathname
(if (string.match pathname ".nix$")
pathname
(.. pathname "/default.nix"))]
(pick-values 1 (string.gsub pathname "^/nix/store/[^/]+/" "modules/"))))
(if (= (pathname:sub 1 1) "/")
(let [pathname (if (string.match pathname ".nix$")
pathname
(.. pathname "/default.nix"))]
(with-open [f (assert (io.open pathname :r))]
(accumulate [lines ""
l (f:lines)
:until (not (= (string.sub l 1 2) "##"))]
(.. lines (string.gsub l "^## *" "") "\n"))))))
(fn strip-newlines [text]
(-> text
(string.gsub "\n([^\n])" " %1")
(string.gsub "\n\n+" "\n")))
(and text
(-> text
(string.gsub "\n([^\n])" " %1")
(string.gsub "\n\n+" "\n"))))
(fn indent [n text]
(let [margin (string.rep " " n)]
(.. margin (string.gsub (or text "") "\n +" (.. "\n" margin )))))
(fn indent-literal [n text]
(let [margin (string.rep " " n)]
(.. margin (string.gsub (or text "") "\n" (.. "\n" margin )))))
(.. margin (string.gsub text "\n +" (.. "\n" margin )))))
(fn extract-text [description]
(match description
{ :_type "literalExpression" : text } text
(where s (= (type s) "string")) description
_ nil))
(fn escape-backslash [s]
(string.gsub s "\\" "\\\\"))
(and description
; (do (print (view description)) true)
(-> (match description
{ :type "literalExpression" : text } text
{} nil
nil nil
t description)
strip-newlines)))
(fn print-option [o offset]
(let [i (or offset 0)]
(print (indent i (.. " * option ``" o.name "``")))
(case (-?> o.description extract-text strip-newlines)
descr (print (indent (+ 4 i) descr)))
(print (indent (+ 4 i)
(or (extract-text o.description) "(no description)")))
(print)
(print (indent (+ 4 i) (.. "**type** " (escape-backslash o.type) "\n")))
(when o.example
(print (indent (+ 4 i) "**example**")) (print)
(print (indent (+ 4 i) ".. code-block:: nix"))
(print)
(print (indent-literal (+ 8 i) (extract-text o.example)) "\n")
(print))
(when (extract-text o.default)
(print (indent (+ 4 i) "**default**")) (print)
(print (indent (+ 4 i) ".. code-block:: nix"))
(print)
(print (indent-literal (+ 8 i) (extract-text o.default)) "\n")
(print))))
(print (indent (+ 4 i) (.. "**type** " o.type "\n")))
(print (indent (+ 4 i)
(.. "**default** "
(or (extract-text (?. o :default)) "(none)")
"\n"
)))
(print )))
(fn print-service [o]
(print (.. " * service ``" o.name "``"))
(match (extract-text o.description)
descr (print (indent 4 descr)))
(print (indent 4 (or (extract-text o.description) "(no description)")))
(print)
(print (indent 4 "**Service parameters**\n"))
(each [_ param (ipairs o.parameters)]
(print-option param 4)))
(fn output? [option]
(match option.loc
["system" "outputs" & _] true
_ false))
(fn is-service? [o]
(= o.type "parametrisable s6-rc service definition"))
(fn has-service? [module]
(accumulate [found false
_ o (ipairs module)]
(or found (is-service? o))))
(fn sort-modules [modules]
;; modules containing services should sort _after_ modules
;; that contain only options
(doto (icollect [n m (pairs modules)]
{ :name n :service? (has-service? m) :module m })
(table.sort
(fn [a b]
(if (and a.service? (not b.service?))
false
(and b.service? (not a.service?))
true
(< a.name b.name))))))
(fn sort-options [module]
(let [options (icollect [_ o (ipairs module)]
(if (not (output? o))
o))]
(doto options (table.sort (fn [a b] (< a.name b.name))))))
(table.sort module (fn [a b] (< a.name b.name)))
module)
(let [raw (yaml.load (io.read "*a"))
modules {}]
@ -120,16 +72,20 @@
(let [e (or (. modules path) [])]
(table.insert e option)
(tset modules path e))))
(tset modules "lib/modules.nix" nil)
(let [modules (sort-modules modules)]
(each [_ {: name : module : service?} (ipairs modules)]
(let [options (sort-options module)]
(when (> (# options) 0)
(print (or (read-preamble name) (headline name)))
(when service?
(print "**path** " (.. ":file:`" (relative-pathname name) "`")))
(print)
(each [_ o (ipairs options)]
(if (is-service? o)
(print-service o)
(print-option o))))))))
(each [name module (pairs modules)]
(print (or (read-preamble name) (headline name)))
(let [options (sort-options module)]
(each [_ o (ipairs options)]
(if (= o.type "parametrisable s6-rc service definition")
(print-service o)
(print-option o))))))
;; for each element el, add to table modules keyed on
;; el.declarations
;; for each value in modules
;; print title
;; elements = (sort elements on el.name)
;; for each el in elements
;; is option or service? print whichever
;;

View File

@ -1,324 +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/uncaught-logs.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-shell --run "liminix-rebuild root@address-of-the-device -I liminix-config=./my-router.nix --arg device "import ./devices/gl-ar750""
(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 :code:`liminix-rebuild`, see the manual section :ref:`admin: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

322
doc/user.rst Normal file
View File

@ -0,0 +1,322 @@
User Manual
###########
This manual is an early work in progress, not least because Liminix is
not yet really ready for users who are not also developers. Your
feedback to improve it is very welcome.
Installation
************
The Liminix installation process is not quite like installing NixOS on
a real computer, but some NixOS experience will nevertheless be
helpful in understanding it. The steps are as follows:
* Decide whether you want the device to be updatable in-place (there
are advantages and disadvantages), or if you are happy to generate
and flash a new image whenever changes are required.
* Create a :file:`configuration.nix` describing the system you want
* Build an image
* Flash it to the device
Supported devices
=================
For a list of devices that Liminix (present or previous versions)
has run on, refer to `devices/ in the source repo <https://gti.telent.net/dan/liminix/src/branch/main/devices>`_. For devices that _currently_ build,
cross-reference it with `the CI status <https://build.liminix.org/jobset/liminix/build#tabs-jobs>`_. Everything that builds is (usually) expected
to run, so if you end up with an image that builds but doesn't
boot, please report it as a bug.
As of June 2023 the device list is a little thin. Adding devices based
on the Atheros or Mediatek (Ralink) platform should be quite
straightforward if you have some C/Linux kernel experience and are
prepared to open it up and attach serial wires: please refer to the
Developer Manual.
Choosing a flavour (read-only or updatable)
===========================================
Liminix installations come in two "flavours"- read-only or in-place
updatable:
* a read-only installation can't be updated once it is flashed to your
device, and so must be reinstalled in its entirety every time you
want to change it. It uses the ``squashfs`` filesystem which has
very good compression ratios and so you can pack quite a lot of
useful stuff onto your device. This is good if you don't expect
to change it often.
* an updatable installation has a writable filesystem so that you can
update configuration, upgrade packages and install new packages over
the network after installation. This uses the `jffs2
<http://www.linux-mtd.infradead.org/doc/jffs2.html>`_ filesystem:
although it does compress the data, the need to support writes means
that it can't pack quite as small as squashfs, so you will not have
as much space to play with.
Updatability caveats
~~~~~~~~~~~~~~~~~~~~
At the time of writing this manual the read-only squashfs support is
much more mature. Consider also that it may not be possible to perform
"larger" updates in-place even if you do opt for updatability. If you
have (for example) an 11MB system on a 16MB device, you won't be able
to do an in-place update of something fundamental like the C library
(libc), as this will temporarily require 22MB to install all the
packages needing the new library before the packages using the old
library can be removed. A writable system will be more useful for
smaller updates such as installing a new package (perhaps you
temporarily need tcpdump to diagnose a network problem) or for
changing configuration files.
Note also that the kernel is not part of the filesystem so cannot be
updated this way. Kernel changes require a full reflash.
Creating configuration.nix
==========================
You need to create a :file:`configuration.nix` that describes your device
and the services that you want to run on it. Start by copying
:file:`vanilla-configuration.nix` and adjusting it, or look in the `examples`
directory for some pre-written configurations.
:file:`configuration.nix` conventionally describes the packages, services,
user accounts etc of the device. It does not describe the hardware
itself, which is specified separately in the build command (as you
will see below).
Your configuration may include modules: it probably *should*
include the ``standard`` module unless you understand what it
does and what happens if you leave it out.
.. code-block:: nix
imports = [
./modules/standard.nix
]
configuration.rootfsType = "jffs2"; # or "squashfs"
Building
========
Build Liminix using the :file:`default.nix` in the project toplevel
directory, passing it arguments for configuration and hardware. For
example:
.. code-block:: console
nix-build -I liminix-config=./tests/smoke/configuration.nix \
--arg device "import ./devices/qemu" -A outputs.default
In this command ``<liminix-config>`` points to your
:file:`configuration.nix`, ``device`` is the file for your hardware device
definition, and ``outputs.default`` will generate some kind of
Liminix image output appropriate to that device.
For the qemu device in this example, ``outputs.default`` is an alias
for ``outputs.vmbuild``, which creates a directory containing a
squashfs root image and a kernel. You can use the :command:`mips-vm` command to
run this.
For the currently supported hardware devices, ``outputs.default``
creates a directory containing a file called ``firmware.bin``. This
is a raw image file that can be written directly to the firmware flash
partition.
Flashing
========
Flashing from the boot monitor
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you are prepared to open the device and have a TTL serial adaptor
of some kind to connect it to, you can probably flash it using U-Boot.
This is quite hardware-specific, and sometimes involves soldering:
please refer to the Developer Manual.
Flashing from an existing Liminix system with :command:`flashcp`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The flash procedure from an existing Liminix-system is two-step.
First we reboot the device (using "kexec") into an "ephemeral"
RAM-based version of the new configuration, then when we're happy it
works we can flash the image - and if it doesn't work we can reboot
the device again and it will boot from the old image.
Building the RAM-based image
............................
To create the ephemeral image, build ``outputs.kexecboot`` instead of
``outputs.default``. This generates a directory containing the root
filesystem image and kernel, along with an executable called `kexec`
and a `boot.sh` script that runs it with appropriate arguments.
For example
.. code-block:: console
nix-build --show-trace -I liminix-config=./examples/arhcive.nix \
--arg device "import ./devices/gl-ar750"
-A outputs.kexecboot && \
(tar chf - result | ssh root@the-device tar -C /run -xvf -)
and then login to the device and run
.. code-block:: console
cd /run/result
sh ./boot.sh .
This will load the new kernel and map the root filesystem into a RAM
disk, then start executing the new kernel. *This is effectively a
reboot - be sure to close all open files and finish anything else
you were doing first.*
If the new system crashes or is rebooted, then the device will revert
to the old configuration it finds in flash.
Building the second (permanent) image
.....................................
While running in the kexecboot system, you can copy the permanent
image to the device with :command:`ssh`
.. code-block:: console
build-machine$ tar chf - result/firmware.bin | \
ssh root@the-device tar -C /run -xvf -
Next you need to 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"
Now run (in this example)
.. code-block:: console
flashcp -v firmware.bin /dev/mtd3
"I know my new image is good, can I skip the intemediate step?"
```````````````````````````````````````````````````````````````
In addition to giving you a chance to see if the new image works, this
two-step process ensures that you're not copying the new image over
the top of the active root filesystem. It might work, or it might
crash in surprising ways.
Flashing from OpenWrt (not currently advised!)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. CAUTION:: At your own risk! This will (at least in some
circumstances) lead to bricking the device: we think this
flash method is currently incompatible with use of a
writeable (jffs2) filesystem.
If your device is running OpenWrt then it probably has the
:command:`mtd` command installed. After transferring the image onto the
device using e.g. :command:`ssh`, you can run it as follows:
.. code-block:: console
mtd -r write /tmp/firmware.bin firmware
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.
Updating an installed system (JFFS2)
************************************
Adding packages
===============
If your device is running a JFFS2 root filesystem, you can build
extra packages for it on your build system and copy them to the
device: any package in Nixpkgs or in the Liminix overlay is available
with the ``pkgs`` prefix:
.. 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 to the device: it doesn't update
any profile to add it to ``$PATH``
Rebuilding the system
=====================
:command:`liminix-rebuild` is the Liminix analogue of :command:`nixos-rebuild`, although its operation is a bit different because it expects to run on a build machine and then copy to the host device. Run it with the same ``liminix-config`` and ``device`` parameters as you would run :command:`nix-build`, and it will build any new/changed packages and then copy them to the device using SSH. For example:
.. code-block:: console
liminix-rebuild root@the-device -I liminix-config=./examples/rotuer.nix --arg device "import ./devices/gl-ar750"
This will
* build anything that needs building
* copy new or changed packages to the device
* reboot the device
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 a lot of things have changed (e.g. a new version of
nixpkgs).
* it cannot upgrade the kernel, only userland
Configuration options
*********************
.. include:: modules.rst

View File

@ -0,0 +1,59 @@
(local { : merge : split : file-exists? : system } (require :anoia))
(local svc (require :anoia.svc))
(fn parse-prefix [str]
(fn parse-extra [s]
(let [out {}]
(each [name val (string.gmatch s ",(.-)=([^,]+)")]
(tset out name val))
out))
(let [(prefix len preferred valid extra)
(string.match str "(.-)::/(%d+),(%d+),(%d+)(.*)$")]
(merge {: prefix : len : preferred : valid} (parse-extra extra))))
;; Format: <prefix>/<length>,preferred,valid[,excluded=<excluded-prefix>/<length>][,class=<prefix class #>]
;;(parse-prefix "2001:8b0:de3a:40dc::/64,7198,7198")
;;(parse-prefix "2001:8b0:de3a:1001::/64,7198,7188,excluded=1/2,thi=10")
(local bound-states
{ :bound true
:rebound true
:informed true
:updated true
:ra-updated true
})
; (local { : view } (require :fennel))
(fn changes [old-prefixes new-prefixes]
(let [added {}
deleted {}
old-set (collect [_ v (ipairs old-prefixes)] (values v true))
new-set (collect [_ v (ipairs new-prefixes)] (values v true))]
(each [_ prefix (ipairs new-prefixes)]
(if (not (. old-set prefix))
(table.insert added (parse-prefix prefix))))
(each [_ prefix (ipairs old-prefixes)]
(if (not (. new-set prefix))
(table.insert deleted (parse-prefix prefix))))
(values added deleted)))
(let [[state-directory lan-device] arg
dir (svc.open state-directory)]
(var prefixes [])
(while true
(while (not (dir:ready?)) (dir:wait))
(if (. bound-states (dir:output "state"))
(let [new-prefixes (split " " (dir:output "/prefixes"))
(added deleted) (changes prefixes new-prefixes)]
(each [_ p (ipairs added)]
(system
(.. "ip address add " p.prefix "::1/" p.len " dev " lan-device)))
(each [_ p (ipairs deleted)]
(system
(.. "ip address del " p.prefix "::1/" p.len " dev " lan-device)))
(set prefixes new-prefixes)))
(dir:wait)))

View File

@ -0,0 +1,8 @@
{
writeFennelScript
, linotify
, anoia
}:
writeFennelScript "acquire-delegated-prefix"
[ linotify anoia ]
./acquire-delegated-prefix.fnl

View File

@ -0,0 +1,60 @@
(local { : merge : split : file-exists? : system } (require :anoia))
(local svc (require :anoia.svc))
;; structurally this is remarkably similar to
;; acquire-lan-prefix.fnl. maybe they should be merged: if not then
;; we could at least extract some common code
;; (alternatively we could move all the parsing code into the thing in
;; the odhcp service that *writes* this stuff)
; (parse-address "2001:8b0:1111:1111:0:ffff:51bb:4cf2/128,3600,7200")
(fn parse-address [str]
(fn parse-extra [s]
(let [out {}]
(each [name val (string.gmatch s ",(.-)=([^,]+)")]
(tset out name val))
out))
(let [(address len preferred valid extra)
(string.match str "(.-)/(%d+),(%d+),(%d+)(.*)$")]
(merge {: address : len : preferred : valid} (parse-extra extra))))
(local bound-states
{ :bound true
:rebound true
:informed true
:updated true
:ra-updated true
})
(fn changes [old-addresses new-addresses]
(let [added {}
deleted {}
old-set (collect [_ v (ipairs old-addresses)] (values v true))
new-set (collect [_ v (ipairs new-addresses)] (values v true))]
(each [_ address (ipairs new-addresses)]
(if (not (. old-set address))
(table.insert added (parse-address address))))
(each [_ address (ipairs old-addresses)]
(if (not (. new-set address))
(table.insert deleted (parse-address address))))
(values added deleted)))
(let [[state-directory wan-device] arg
dir (svc.open state-directory)]
(var addresses [])
(while true
(while (not (dir:ready?)) (dir:wait))
(if (. bound-states (dir:output "state"))
(let [new-addresses (split " " (dir:output "/addresses"))
(added deleted) (changes addresses new-addresses)]
(each [_ p (ipairs added)]
(system
(.. "ip address add " p.address "/" p.len " dev " wan-device)))
(each [_ p (ipairs deleted)]
(system
(.. "ip address del " p.address "/" p.len " dev " wan-device)))
(set addresses new-addresses)))
(dir:wait)))

View File

@ -0,0 +1,8 @@
{
writeFennelScript
, linotify
, anoia
}:
writeFennelScript "acquire-wan-address"
[ linotify anoia ]
./acquire-wan-address.fnl

View File

@ -11,10 +11,16 @@
...
}: let
secrets = import ./extneder-secrets.nix;
inherit
(pkgs.liminix.networking)
address
udhcpc
interface
route
;
inherit (pkgs.liminix.services) oneshot longrun bundle target;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) writeText dropbear ifwait serviceFns;
svc = config.system.service;
in rec {
boot = {
tftp = {
@ -24,29 +30,87 @@ in rec {
};
imports = [
../modules/standard.nix
../modules/wlan.nix
../modules/network
../modules/vlan
../modules/ssh
../modules/usb.nix
../modules/watchdog
../modules/mount
];
hostname = "arhcive";
kernel = {
config = {
USB = "y";
USB_EHCI_HCD = "y";
USB_EHCI_HCD_PLATFORM = "y";
USB_OHCI_HCD = "y";
USB_OHCI_HCD_PLATFORM = "y";
USB_SUPPORT = "y";
USB_COMMON = "y";
USB_STORAGE = "y";
USB_STORAGE_DEBUG = "n";
USB_UAS = "y";
USB_ANNOUNCE_NEW_DEVICES = "y";
SCSI = "y";
BLK_DEV_SD = "y";
USB_PRINTER = "y";
PARTITION_ADVANCED = "y";
MSDOS_PARTITION = "y";
EFI_PARTITION = "y";
EXT4_FS = "y";
EXT4_USE_FOR_EXT2 = "y";
FS_ENCRYPTION = "y";
};
};
services.dhcpc =
let iface = config.hardware.networkInterfaces.lan;
in svc.network.dhcp.client.build {
interface = iface;
let iface = config.hardware.networkInterfaces.lan;
in (udhcpc iface {
dependencies = [ config.services.hostname ];
}) // { inherit (iface) device; };
services.sshd = longrun {
name = "sshd";
run = ''
mkdir -p /run/dropbear
${dropbear}/bin/dropbear -E -P /run/dropbear.pid -R -F
'';
};
services.watchdog =
let
watched = with config.services ; [ sshd dhcpc ];
spinupGrace = 60;
script = pkgs.writeAshScript "gaspode" {
runtimeInputs = [ pkgs.s6 ];
} ''
deadline=$(expr $(date +%s) + ${toString spinupGrace})
services=$@
echo started feeding the dog
exec 3> ''${WATCHDOG-/dev/watchdog}
healthy(){
test $(date +%s) -le $deadline && return 0
for i in $services; do
if test "$(s6-svstat -o up /run/service/$i)" != "true" ; then
echo "service $i is down"
return 1
fi
done
}
while healthy ;do
sleep 10
echo >&3
done
echo "stopped feeding the dog"
sleep 6000 # don't want s6-rc to restart
'';
in longrun {
name = "watchdog";
run =
"${script} ${lib.concatStringsSep " " (builtins.map (s: s.name) watched)}";
};
services.sshd = svc.ssh.build { };
services.watchdog = svc.watchdog.build {
watched = with config.services ; [ sshd dhcpc ];
};
services.resolvconf = oneshot rec {
dependencies = [ services.dhcpc ];
@ -59,6 +123,9 @@ in rec {
done
)
'';
down = ''
rm -rf /run/service-state/${name}/
'';
};
filesystem = dir {
etc = dir {
@ -67,25 +134,35 @@ in rec {
srv = dir {};
};
services.defaultroute4 = svc.network.route.build {
services.defaultroute4 = route {
name = "defaultroute";
via = "$(output ${services.dhcpc} router)";
target = "default";
dependencies = [services.dhcpc];
};
programs.busybox = {
applets = ["lsusb" "tar"];
applets = ["blkid" "lsusb" "findfs" "tar"];
options = {
FEATURE_LS_TIMESTAMPS = "y";
FEATURE_LS_SORTFILES = "y";
FEATURE_BLKID_TYPE = "y";
FEATURE_MOUNT_FLAGS = "y";
FEATURE_MOUNT_LABEL = "y";
FEATURE_VOLUMEID_EXT = "y";
};
};
services.mount_external_disk = svc.mount.build {
partlabel = "backup-disk";
mountpoint = "/srv";
fstype = "ext4";
services.mount_external_disk = oneshot {
name = "mount_external_disk";
up = ''
while ! findfs LABEL=backup-disk; do
echo waiting for backup-disk
sleep 1
done
mount -t ext4 LABEL=backup-disk /srv
'';
down = "umount /srv";
};
services.rsync =
@ -122,8 +199,22 @@ in rec {
] ;
};
services.default = target {
name = "default";
contents =
let links = config.hardware.networkInterfaces;
in with config.services; [
links.lo
defaultroute4
resolvconf
sshd
rsync
watchdog
];
};
users.root = {
passwd = lib.mkForce secrets.root.passwd;
passwd = lib.mkForce secrets.root_password;
# openssh.authorizedKeys.keys = [
# (builtins.readFile "/home/dan/.ssh/id_rsa.pub")
# ];
@ -137,17 +228,5 @@ in rec {
gid=500; usernames = ["backup"];
};
defaultProfile.packages = with pkgs; [
e2fsprogs
mtdutils
(levitate.override {
config = {
services = {
inherit (config.services) dhcpc sshd watchdog;
};
defaultProfile.packages = [ mtdutils ];
users.root.openssh.authorizedKeys.keys = secrets.root.keys;
};
})
];
defaultProfile.packages = with pkgs; [e2fsprogs strace tcpdump ];
}

View File

@ -1,201 +0,0 @@
# This is an example configuration for a "typical" small office/home
# router and wifi access point.
# You need to copy it to another filename and change the configuration
# wherever the text "EDIT" appears - please consult the tutorial
# documentation for details.
{ config, pkgs, lib, ... } :
let
inherit (pkgs.liminix.services) bundle oneshot longrun;
inherit (pkgs) serviceFns;
# EDIT: you can pick your preferred RFC1918 address space
# for NATted connections, if you don't like this one.
ipv4LocalNet = "10.8.0";
svc = config.system.service;
in rec {
boot = {
tftp = {
freeSpaceBytes = 3 * 1024 * 1024;
serverip = "10.0.0.1";
ipaddr = "10.0.0.8";
};
};
imports = [
../modules/bridge
../modules/dhcp6c
../modules/dnsmasq
../modules/firewall
../modules/hostapd
../modules/network
../modules/ntp
../modules/ppp
../modules/ssh
../modules/vlan
../modules/wlan.nix
];
rootfsType = "jffs2";
hostname = "the-internet"; # EDIT
services.hostap = svc.hostapd.build {
interface = config.hardware.networkInterfaces.wlan;
# EDIT: you will want to change the obvious things
# here to values of your choice
params = {
ssid = "the-internet";
channel = "1";
country_code = "GB";
wpa_passphrase = "not a real wifi password";
hw_mode="g";
ieee80211n = 1;
auth_algs = 1; # 1=wpa2, 2=wep, 3=both
wpa = 2; # 1=wpa, 2=wpa2, 3=both
wpa_key_mgmt = "WPA-PSK";
wpa_pairwise = "TKIP CCMP"; # auth for wpa (may not need this?)
rsn_pairwise = "CCMP"; # auth for wpa2
wmm_enabled = 1;
};
};
services.int = svc.network.address.build {
interface = svc.bridge.primary.build { ifname = "int"; };
family = "inet"; address = "${ipv4LocalNet}.1"; prefixLength = 16;
};
services.bridge = svc.bridge.members.build {
primary = services.int;
members = with config.hardware.networkInterfaces;
[ wlan lan ];
};
services.ntp = svc.ntp.build {
pools = { "pool.ntp.org" = ["iburst"]; };
makestep = { threshold = 1.0; limit = 3; };
};
services.sshd = svc.ssh.build { };
users.root = {
# EDIT: choose a root password and then use
# "mkpasswd -m sha512crypt" to determine the hash.
# It should start wirh $6$.
passwd = "$6$6HG7WALLQQY1LQDE$428cnouMJ7wVmyK9.dF1uWs7t0z9ztgp3MHvN5bbeo0M4Kqg/u2ThjoSHIjCEJQlnVpDOaEKcOjXAlIClHWN21";
openssh.authorizedKeys.keys = [
# EDIT: you can add your ssh pubkey here
# "ssh-rsa AAAAB3NzaC1....H6hKd user@example.com";
];
};
services.dns =
let interface = services.int;
in svc.dnsmasq.build {
resolvconf = services.resolvconf;
inherit interface;
ranges = [
"${ipv4LocalNet}.10,${ipv4LocalNet}.249"
# EDIT: ... maybe. In this example we use "ra-stateless",
# meaning dnsmasq sends router advertisements with the O and A
# bits set, and provides a stateless DHCP service. The client
# will use a SLAAC address, and use DHCP for other
# configuration information.
# If you didn't understand the preceding sentence then
# the default is _probably_ fine, but if you need
# a DHCP-only IPv6 network or some other different
# configuration, this is the place to change it.
"::,constructor:$(output ${interface} ifname),ra-stateless"
];
# EDIT: choose a domain name for the DNS names issued for your
# DHCP-issued hosts
domain = "lan.example.com";
};
services.wan = svc.pppoe.build {
interface = config.hardware.networkInterfaces.wan;
ppp-options = [
"debug" "+ipv6" "noauth"
# EDIT: change the strings "chap-username"
# and "chap-secret" to match the username/password
# provided by your ISP for PPP logins
"name" "chap-username"
"password" "chap-secret"
];
};
services.resolvconf = oneshot rec {
dependencies = [ services.wan ];
name = "resolvconf";
up = ''
. ${serviceFns}
( in_outputs ${name}
echo "nameserver $(output ${services.wan} ns1)" > resolv.conf
echo "nameserver $(output ${services.wan} ns2)" >> resolv.conf
chmod 0444 resolv.conf
)
'';
};
filesystem =
let inherit (pkgs.pseudofile) dir symlink;
in dir {
etc = dir {
"resolv.conf" = symlink "${services.resolvconf}/.outputs/resolv.conf";
};
};
services.defaultroute4 = svc.network.route.build {
via = "$(output ${services.wan} address)";
target = "default";
dependencies = [ services.wan ];
};
services.defaultroute6 = svc.network.route.build {
via = "$(output ${services.wan} ipv6-peer-address)";
target = "default";
interface = services.wan;
};
services.firewall = svc.firewall.build {
};
services.packet_forwarding = svc.network.forward.build { };
# We expect the ISP uses DHCP6 to issue IPv6 addresses. There is a
# service to request address information in the form of a DHCP
# lease, and two dependent services that listen for updates to the
# DHCP address information and update the addresses of the WAN and
# LAN interfaces respectively.
services.dhcp6c =
let client = svc.dhcp6c.client.build {
interface = services.wan;
};
in bundle {
name = "dhcp6c";
contents = [
(svc.dhcp6c.prefix.build {
# if your ISP provides you a real IPv6 prefix for your local
# network (usually a /64 or /48 or something in between the
# two), this service subscribes to that "prefix delegation"
# information, and uses it to assign an address to the LAN
# device. dnsmasq will notice this address and use it to
# form the addresses it hands out to devices on the lan
inherit client;
interface = services.int;
})
(svc.dhcp6c.address.build {
# if your ISP provides you a regular global IPv6 address,
# this service subscribes to that information and assigns
# the address to the WAN device.
inherit client;
interface = services.wan;
})
];
};
defaultProfile.packages = with pkgs; [
min-collect-garbage
];
}

View File

@ -8,11 +8,21 @@
config,
pkgs,
lib,
modulesPath,
...
}: let
secrets = import ./extneder-secrets.nix;
svc = config.system.service;
inherit
(pkgs.liminix.networking)
address
udhcpc
hostapd
interface
route
;
inherit (pkgs.liminix.services) oneshot longrun bundle target;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) dropbear ifwait serviceFns
;
in rec {
boot = {
tftp = {
@ -22,32 +32,141 @@ in rec {
};
imports = [
"${modulesPath}/profiles/wap.nix"
"${modulesPath}/vlan"
"${modulesPath}/ssh"
../modules/wlan.nix
../modules/standard.nix
];
hostname = "extneder";
profile.wap = {
interfaces = with config.hardware.networkInterfaces; [
lan
wlan
];
kernel = {
config = {
wireless = {
networks.${secrets.ssid} = {
interface = config.hardware.networkInterfaces.wlan;
inherit (secrets) channel wpa_passphrase;
country_code = "GB";
hw_mode = "g";
wmm_enabled = 1;
ieee80211n = 1;
};
NETFILTER_XT_MATCH_CONNTRACK = "y";
IP6_NF_IPTABLES = "y"; # do we still need these
IP_NF_IPTABLES = "y"; # if using nftables directly
# these are copied from rotuer and need review
IP_NF_NAT = "y";
IP_NF_TARGET_MASQUERADE = "y";
NETFILTER = "y";
NETFILTER_ADVANCED = "y";
NETFILTER_XTABLES = "y";
NFT_COMPAT = "y";
NFT_CT = "y";
NFT_LOG = "y";
NFT_MASQ = "y";
NFT_NAT = "y";
NFT_REJECT = "y";
NFT_REJECT_INET = "y";
NF_CONNTRACK = "y";
NF_NAT = "y";
NF_NAT_MASQUERADE = "y";
NF_TABLES = "y";
NF_TABLES_INET = "y";
NF_TABLES_IPV4 = "y";
NF_TABLES_IPV6 = "y";
};
};
services.sshd = svc.ssh.build {};
users.root.passwd = lib.mkForce secrets.root.passwd;
services.hostap = hostapd (config.hardware.networkInterfaces.wlan) {
params = {
country_code = "GB";
hw_mode = "g";
wmm_enabled = 1;
ieee80211n = 1;
inherit (secrets) ssid channel wpa_passphrase;
auth_algs = 1; # 1=wpa2, 2=wep, 3=both
wpa = 2; # 1=wpa, 2=wpa2, 3=both
wpa_key_mgmt = "WPA-PSK";
wpa_pairwise = "TKIP CCMP"; # auth for wpa (may not need this?)
rsn_pairwise = "CCMP"; # auth for wpa2
};
};
services.int = interface {
type = "bridge";
device = "int";
};
services.dhcpc = (udhcpc services.int {
dependencies = [ config.services.hostname ];
}) // { device = "int"; };
services.bridge = let
primary = services.int;
addif = dev:
oneshot {
name = "add-${dev.device}-to-bridge";
up = "${ifwait}/bin/ifwait -v ${dev.device} running && ip link set dev ${dev.device} master ${primary.device}";
down = "ip link set dev ${dev} nomaster";
dependencies = [primary dev];
};
in
bundle {
name = "bridge-members";
contents = with config.hardware.networkInterfaces;
map addif [
lan
wlan
];
};
services.sshd = longrun {
name = "sshd";
run = ''
mkdir -p /run/dropbear
${dropbear}/bin/dropbear -E -P /run/dropbear.pid -R -F
'';
};
services.resolvconf = oneshot rec {
dependencies = [ services.dhcpc ];
name = "resolvconf";
# CHECK: https://udhcp.busybox.net/README.udhcpc says
# 'A list of DNS server' but doesn't say what separates the
# list members. Assuming it's a space or other IFS character
up = ''
. ${serviceFns}
( in_outputs ${name}
for i in $(output ${services.dhcpc} dns); do
echo "nameserver $i" > resolv.conf
done
)
'';
down = ''
rm -rf /run/service-state/${name}/
'';
};
filesystem = dir {
etc = dir {
"resolv.conf" = symlink "${services.resolvconf}/.outputs/resolv.conf";
};
};
services.defaultroute4 = route {
name = "defaultroute";
via = "$(output ${services.dhcpc} router)";
target = "default";
dependencies = [services.dhcpc];
};
services.default = target {
name = "default";
contents =
let links = config.hardware.networkInterfaces;
in with config.services; [
links.lo links.eth links.wlan
int
bridge
hostap
defaultroute4
resolvconf
sshd
];
};
users.root.passwd = lib.mkForce secrets.root_password;
defaultProfile.packages = with pkgs; [nftables strace tcpdump swconfig];
}

View File

@ -1,42 +0,0 @@
{ config, pkgs, lib, ... } :
let
inherit (pkgs) serviceFns;
svc = config.system.service;
in rec {
imports = [
../modules/network
../modules/ssh
../modules/vlan
];
boot.tftp = {
# IP addresses to use in the boot monitor when flashing/ booting
# over TFTP. If you are flashing using the stock firmware's Web UI
# then these dummy values are fine
ipaddr = "192.0.2.115"; # my address
serverip = "192.0.2.5"; # build machine or other tftp server
};
hostname = "hello";
services.dhcpc = svc.network.dhcp.client.build {
interface = config.hardware.networkInterfaces.lan;
# don't start DHCP until the hostname is configured,
# so it can identify itself to the DHCP server
dependencies = [ config.services.hostname ];
};
services.sshd = svc.ssh.build { };
users.root = {
# the password is "secret". Use mkpasswd -m sha512crypt to
# create this hashed password string
passwd = "$6$y7WZ5hM6l5nriLmo$5AJlmzQZ6WA.7uBC7S8L4o19ESR28Dg25v64/vDvvCN01Ms9QoHeGByj8lGlJ4/b.dbwR9Hq2KXurSnLigt1W1";
};
defaultProfile.packages = with pkgs; [
figlet
];
}

View File

@ -1,44 +0,0 @@
{ config, pkgs, lib, ... } :
let
inherit (pkgs) serviceFns;
svc = config.system.service;
in rec {
imports = [
../modules/network
../modules/dnsmasq
../modules/ssh
];
hostname = "hello";
# configure the internal network (LAN) with an address
services.int = svc.network.address.build {
interface = config.hardware.networkInterfaces.lan;
family = "inet"; address ="10.3.0.1"; prefixLength = 16;
};
services.sshd = svc.ssh.build { };
users.root = {
# the password is "secret". Use mkpasswd -m sha512crypt to
# create this hashed password string
passwd = "$6$y7WZ5hM6l5nriLmo$5AJlmzQZ6WA.7uBC7S8L4o19ESR28Dg25v64/vDvvCN01Ms9QoHeGByj8lGlJ4/b.dbwR9Hq2KXurSnLigt1W1";
};
services.dns =
let interface = services.int;
in svc.dnsmasq.build {
inherit interface;
ranges = [
"10.3.0.10,10.3.0.240"
"::,constructor:$(output ${interface} ifname),ra-stateless"
];
domain = "example.org";
};
defaultProfile.packages = with pkgs; [
figlet
];
}

View File

@ -1,120 +0,0 @@
{ config, pkgs, ... } :
let
inherit (pkgs.liminix.services) oneshot longrun bundle target;
inherit (pkgs) writeText;
svc = config.system.service;
secrets-1 = {
ssid = "Zyxel 2G (N)";
wpa_passphrase = "diamond dogs";
};
secrets-2 = {
ssid = "Zyxel 5G (AX)";
wpa_passphrase = "diamond dogs";
};
baseParams = {
country_code = "FR";
hw_mode = "g";
channel = 6;
wmm_enabled = 1;
ieee80211n = 1;
ht_capab = "[LDPC][GF][HT40-][HT40+][SHORT-GI-40][MAX-AMSDU-7935][TX-STBC]";
auth_algs = 1;
wpa = 2;
wpa_key_mgmt = "WPA-PSK";
wpa_pairwise = "TKIP CCMP";
rsn_pairwise = "CCMP";
};
modernParams = {
hw_mode = "a";
he_su_beamformer = 1;
he_su_beamformee = 1;
he_mu_beamformer = 1;
preamble = 1;
# Allow radar detection.
ieee80211d = 1;
ieee80211h = 1;
ieee80211ac = 1;
ieee80211ax = 1;
vht_capab = "[MAX-MPDU-7991][SU-BEAMFORMEE][SU-BEAMFORMER][RXLDPC][SHORT-GI-80][MAX-A-MPDU-LEN-EXP3][RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN][TX-STBC-2BY1][RX-STBC-1][MU-BEAMFORMER]";
vht_oper_chwidth = 1;
he_oper_chwidth = 1;
channel = 36;
vht_oper_centr_freq_seg0_idx = 42;
he_oper_centr_freq_seg0_idx = 42;
require_vht = 1;
};
mkWifiSta = params: interface: secrets: svc.hostapd.build {
inherit interface;
params = params // {
inherit (secrets) ssid wpa_passphrase;
};
};
in rec {
imports = [
../modules/wlan.nix
../modules/network
../modules/hostapd
../modules/ssh
../modules/ntp
../modules/vlan
../modules/bridge
];
hostname = "zyxel";
users.root = {
# EDIT: choose a root password and then use
# "mkpasswd -m sha512crypt" to determine the hash.
# It should start wirh $6$.
passwd = "$y$j9T$f8GhLiqYmr3lc58eKhgyD0$z7P/7S9u.kq/cANZExxhS98bze/6i7aBxU6tbl7RMi.";
openssh.authorizedKeys.keys = [
# EDIT: you can add your ssh pubkey here
# "ssh-rsa AAAAB3NzaC1....H6hKd user@example.com";
];
};
services.int = svc.bridge.primary.build {
ifname = "int";
};
services.bridge = svc.bridge.members.build {
primary = services.int;
members = with config.hardware.networkInterfaces; [
lan
wlan0
wlan1
];
};
services.dhcpv4 =
let iface = services.int;
in svc.network.dhcp.client.build { interface = iface; };
services.defaultroute4 = svc.network.route.build {
via = "$(output ${services.dhcpv4} address)";
target = "default";
dependencies = [ services.dhcpv4 ];
};
services.packet_forwarding = svc.network.forward.build { };
services.sshd = svc.ssh.build {
allowRoot = true;
};
services.ntp = config.system.service.ntp.build {
pools = { "pool.ntp.org" = ["iburst"] ; };
};
boot.tftp = {
serverip = "192.0.2.10";
ipaddr = "192.0.2.12";
};
# wlan0 is the 2.4GHz interface.
services.hostap-1 = mkWifiSta baseParams config.hardware.networkInterfaces.wlan0 secrets-1;
# wlan1 is the 5GHz interface, e.g. AX capable.
services.hostap-2 = mkWifiSta (baseParams // modernParams) config.hardware.networkInterfaces.wlan1 secrets-2;
defaultProfile.packages = with pkgs; [ zyxel-bootconfig iw min-collect-garbage mtdutils ];
}

View File

@ -1,116 +0,0 @@
{ config, pkgs, lib, ... } :
let
inherit (pkgs) serviceFns;
svc = config.system.service;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs.liminix.services) oneshot longrun bundle target;
some-util-linux = pkgs.runCommand "some-util-linux" {} ''
mkdir -p $out/bin
cd ${pkgs.util-linux-small}/bin
cp fdisk sfdisk mkswap $out/bin
'';
in rec {
imports = [
../modules/network
../modules/ssh
../modules/usb.nix
../modules/schnapps
../modules/outputs/mtdimage.nix
../modules/outputs/mbrimage.nix
../modules/outputs/tftpboot.nix
../modules/outputs/ubifs.nix
../modules/outputs/ubimage.nix
../modules/outputs/jffs2.nix
../modules/outputs/ext4fs.nix
../modules/outputs/extlinux.nix
];
kernel.config = {
BTRFS_FS = "y";
};
boot.tftp = {
ipaddr = "10.0.0.8"; # my address
serverip = "10.0.0.1"; # build machine or other tftp server
freeSpaceBytes = 1024 * 1024 * 4;
};
boot.loader.extlinux.enable = true;
hostname = "liminix-recovery";
services.dhcpc = svc.network.dhcp.client.build {
interface = config.hardware.networkInterfaces.lan2;
# don't start DHCP until the hostname is configured,
# so it can identify itself to the DHCP server
dependencies = [ config.services.hostname ];
};
services.sshd = svc.ssh.build {
dependencies = [ config.services.growfs ];
};
services.defaultroute4 = svc.network.route.build {
via = "$(output ${services.dhcpc} router)";
target = "default";
dependencies = [services.dhcpc];
};
services.resolvconf = oneshot rec {
dependencies = [ services.dhcpc ];
name = "resolvconf";
up = ''
. ${serviceFns}
( in_outputs ${name}
for i in $(output ${services.dhcpc} dns); do
echo "nameserver $i" > resolv.conf
done
)
'';
};
services.growfs = let name = "growfs"; in oneshot {
inherit name;
up = ''
. ${serviceFns}
device=$(grep /persist /proc/1/mountinfo | cut -f9 -d' ')
${pkgs.e2fsprogs}/bin/resize2fs $device
'';
};
filesystem = dir {
etc = dir {
"resolv.conf" = symlink "${services.resolvconf}/.outputs/resolv.conf";
};
mnt = dir {};
};
rootfsType = "ext4";
# sda is most likely correct for the boot-from-USB case. For tftp
# it's overridden by the boot.scr anyway, so maybe it all works out
hardware.rootDevice = lib.mkForce "/dev/sda1";
users.root = {
# the password is "secret". Use mkpasswd -m sha512crypt to
# create this hashed password string
passwd = "$6$y7WZ5hM6l5nriLmo$5AJlmzQZ6WA.7uBC7S8L4o19ESR28Dg25v64/vDvvCN01Ms9QoHeGByj8lGlJ4/b.dbwR9Hq2KXurSnLigt1W1";
openssh.authorizedKeys.keys =
let fromBuild =
(builtins.readFile
((builtins.toPath (builtins.getEnv "HOME")) + "/.ssh/authorized_keys")
);
in lib.splitString "\n" fromBuild;
};
defaultProfile.packages = with pkgs; [
e2fsprogs # ext4
btrfs-progs
mtdutils # mtd, jffs2, ubifs
dtc # you never know when you might need device tree stuff
some-util-linux
libubootenv # fw_{set,print}env
pciutils
];
}

View File

@ -35,7 +35,6 @@ in {
(drop "icmpv6 type destination-unreachable ct state invalid,untracked")
];
};
forward-ip6 = {
type = "filter";
family = "ip6";
@ -96,23 +95,19 @@ in {
# recognised (outbound-initiated) flow
(accept "oifname \"int\" iifname \"ppp0\" ct state established,related")
(accept "iifname \"int\" oifname \"ppp0\" ")
"log prefix \"DENIED CHAIN=forward-ip6 \""
];
};
input-ip6-lan = {
input-lan = {
type = "filter";
family = "ip6";
rules = [
(accept "udp dport 547") # dhcp, could restrict to daddr ff02::1:2
(accept "udp dport 53") # dns
(accept "tcp dport 22")
];
};
input-ip6-wan = {
input-wan = {
type = "filter";
family = "ip6";
@ -128,8 +123,8 @@ in {
hook = "input";
rules = [
(accept "meta l4proto icmpv6")
"iifname int jump input-ip6-lan"
"iifname ppp0 jump input-ip6-wan"
"iifname int jump input-lan"
"iifname ppp0 jump input-wan"
(if allow-incoming
then accept "oifname \"int\" iifname \"ppp0\""
else "oifname \"int\" iifname \"ppp0\" jump incoming-allowed-ip6"
@ -137,7 +132,6 @@ in {
# how does this even make sense in an input chain?
(accept "oifname \"int\" iifname \"ppp0\" ct state established,related")
(accept "iifname \"int\" oifname \"ppp0\" ")
"log prefix \"DENIED CHAIN=input-ip6 \""
];
};
@ -160,7 +154,6 @@ in {
"oifname \"ppp0\" masquerade"
];
};
nat-rx = {
type = "nat";
hook = "prerouting";
@ -174,71 +167,4 @@ in {
# packet replies. "
];
};
# these chains are for rules that have to be present for things to
# basically work at all: for example, the router won't issue DHCP
# unless it's allowed to receive DHCP requests. For "site policy"
# rules you may prefer to use incoming-allowed-ip[46] instead
input-ip4-lan = {
type = "filter";
family = "ip";
rules = [
(accept "udp dport 67") # dhcp
(accept "udp dport 53") # dns
(accept "tcp dport 22") # ssh
];
};
input-ip4-wan = {
type = "filter";
family = "ip";
rules = [
(accept "udp sport 53")
];
};
input-ip4 = {
type = "filter";
family = "ip";
policy = "drop";
hook = "input";
rules = [
"iifname lo accept"
"icmp type { echo-request, echo-reply } accept"
"iifname int jump input-ip4-lan"
"iifname ppp0 jump input-ip4-wan"
"oifname \"int\" iifname \"ppp0\" jump incoming-allowed-ip4"
"ct state established,related accept"
"log prefix \"DENIED CHAIN=input-ip4 \""
];
};
forward-ip4 = {
type = "filter";
family = "ip";
policy = "drop";
hook = "forward";
rules = [
"iifname \"int\" accept"
"ct state established,related accept"
"oifname \"int\" iifname \"ppp0\" jump incoming-allowed-ip4"
"log prefix \"DENIED CHAIN=forward-ip4 \""
];
};
incoming-allowed-ip4 = {
type = "filter";
family = "ip";
rules = [
# This is where you put permitted incoming connections. If
# you're using NAT and want to forward a port from outside to
# devices on the LAN, then you need a DNAT rule in nat-rx chain
# *and* to accept the packet in this chain (specifying the
# internal (RFC1918) address).
];
};
}

View File

@ -1,6 +1,5 @@
{
rec {
wpa_passphrase = "you bring light in";
ssid = "liminix";
l2tp = {
name = "abcde@a.1";
password = "NotMyIspPassword";
@ -11,9 +10,5 @@
openssh.authorizedKeys.keys = [
];
};
lan = {
prefix = "10.8.0";
};
root_password = root.passwd;
}

View File

@ -6,18 +6,29 @@
# problems.
{ config, pkgs, lib, modulesPath, ... } :
{ config, pkgs, lib, ... } :
let
secrets = {
domainName = "fake.liminix.org";
firewallRules = {};
} // (import ./rotuer-secrets.nix);
inherit (pkgs.liminix.services) oneshot bundle;
inherit (pkgs) serviceFns;
secrets = import ./rotuer-secrets.nix;
inherit (pkgs.liminix.networking)
address
interface
route;
inherit (pkgs.liminix.services) oneshot longrun bundle target;
inherit (pkgs)
dropbear
ifwait
writeText
writeFennelScript
serviceFns;
svc = config.system.service;
wirelessConfig = {
country_code = "GB";
inherit (secrets) wpa_passphrase;
auth_algs = 1; # 1=wpa2, 2=wep, 3=both
wpa = 2; # 1=wpa, 2=wpa2, 3=both
wpa_key_mgmt = "WPA-PSK";
wpa_pairwise = "TKIP CCMP"; # auth for wpa (may not need this?)
rsn_pairwise = "CCMP"; # auth for wpa2
wmm_enabled = 1;
};
@ -31,62 +42,52 @@ in rec {
};
imports = [
"${modulesPath}/profiles/gateway.nix"
"${modulesPath}/schnapps"
"${modulesPath}/outputs/btrfs.nix"
"${modulesPath}/outputs/extlinux.nix"
../modules/wlan.nix
../modules/standard.nix
../modules/ppp
../modules/dnsmasq
../modules/firewall
../modules/hostapd
../modules/bridge
../modules/ntp
];
rootfsType = "jffs2";
hostname = "rotuer";
rootfsType = "btrfs";
rootOptions = "subvol=@";
boot.loader.extlinux.enable = true;
profile.gateway = {
lan = {
interfaces = with config.hardware.networkInterfaces;
[
wlan wlan5
lan0 lan1 lan2 lan3 lan4
];
inherit (secrets.lan) prefix;
address = {
family = "inet"; address ="${secrets.lan.prefix}.1"; prefixLength = 24;
};
dhcp = {
start = 10;
end = 240;
hosts = { } // lib.optionalAttrs (builtins.pathExists ./static-leases.nix) (import ./static-leases.nix);
localDomain = "lan";
};
};
wan = {
interface = config.hardware.networkInterfaces.wan;
username = secrets.l2tp.name;
password = secrets.l2tp.password;
dhcp6.enable = true;
};
firewall = {
enable = true;
rules = secrets.firewallRules;
};
wireless.networks = {
"${secrets.ssid}" = {
interface = config.hardware.networkInterfaces.wlan;
hw_mode="g";
channel = "2";
ieee80211n = 1;
} // wirelessConfig;
"${secrets.ssid}5" = rec {
interface = config.hardware.networkInterfaces.wlan5;
hw_mode="a";
channel = 36;
ht_capab = "[HT40+]";
vht_oper_chwidth = 1;
vht_oper_centr_freq_seg0_idx = channel + 6;
ieee80211n = 1;
ieee80211ac = 1;
} // wirelessConfig;
services.hostap = svc.hostapd.build {
interface = config.hardware.networkInterfaces.wlan_24;
params = {
ssid = "liminix";
hw_mode="g";
channel = "2";
ieee80211n = 1;
} // wirelessConfig;
};
services.hostap5 = svc.hostapd.build {
interface = config.hardware.networkInterfaces.wlan_5;
params = rec {
ssid = "liminix_5";
hw_mode="a";
channel = 36;
ht_capab = "[HT40+]";
vht_oper_chwidth = 1;
vht_oper_centr_freq_seg0_idx = channel + 6;
ieee80211ac = 1;
} // wirelessConfig;
};
services.int =
let iface = svc.bridge.primary.build { ifname = "int"; };
in address iface {
family = "inet4"; address ="10.8.0.1"; prefixLength = 16;
};
services.bridge = svc.bridge.members.build {
primary = services.int;
members = with config.hardware.networkInterfaces; [
wlan_24 lan wlan_5
];
};
services.ntp = svc.ntp.build {
@ -94,24 +95,153 @@ in rec {
makestep = { threshold = 1.0; limit = 3; };
};
services.sshd = svc.ssh.build { };
services.sshd = longrun {
name = "sshd";
# env -i clears the environment so we don't pass anything weird to
# ssh sessions. Dropbear params are
# -e pass environment to child
# -E log to stderr
# -R create hostkeys if needed
# -P pid-file
# -F don't fork into background
run = ''
if test -d /persist; then
mkdir -p /persist/secrets/dropbear
ln -s /persist/secrets/dropbear /run
fi
PATH=${lib.makeBinPath config.defaultProfile.packages}:/bin
exec env -i ENV=/etc/ashrc PATH=$PATH ${dropbear}/bin/dropbear -e -E -R -P /run/dropbear.pid -F
'';
};
users.root = secrets.root;
services.dns =
let interface = services.int;
in svc.dnsmasq.build {
resolvconf = services.resolvconf;
inherit interface;
ranges = [
"10.8.0.10,10.8.0.240"
"::,constructor:${interface.device},ra-stateless"
];
domain = "fake.liminix.org";
};
services.wan = svc.pppoe {
interface = config.hardware.networkInterfaces.wan;
ppp-options = [
"debug" "+ipv6" "noauth"
"name" secrets.l2tp.name
"password" secrets.l2tp.password
];
};
services.resolvconf = oneshot rec {
dependencies = [ services.wan ];
name = "resolvconf";
up = ''
. ${serviceFns}
( in_outputs ${name}
echo "nameserver $(output ${services.wan} ns1)" > resolv.conf
echo "nameserver $(output ${services.wan} ns2)" >> resolv.conf
)
'';
down = ''
rm -rf /run/service-state/${name}/
'';
};
services.defaultroute4 = route {
name = "defaultroute4";
via = "$(output ${services.wan} address)";
target = "default";
dependencies = [ services.wan ];
};
services.defaultroute6 = route {
name = "defaultroute6";
via = "$(output ${services.wan} ipv6-peer-address)";
target = "default";
dev = "$(output ${services.wan} ifname)";
dependencies = [ services.wan ];
};
services.firewall = svc.firewall.build {
ruleset = import ./rotuer-firewall.nix;
};
services.packet_forwarding =
let
ip4 = "/proc/sys/net/ipv4/conf/all/forwarding";
ip6 = "/proc/sys/net/ipv6/conf/all/forwarding";
in oneshot {
name = "let-the-ip-flow";
up = ''
echo 1 > ${ip4}
echo 1 > ${ip6}
'';
down = ''
echo 0 > ${ip4};
echo 0 > ${ip6};
'';
dependencies = [ services.firewall ];
};
services.dhcp6 =
let
name = "dhcp6c.wan";
in longrun {
inherit name;
notification-fd = 10;
run = ''
export SERVICE_STATE=/run/service-state/${name}
${pkgs.odhcp6c}/bin/odhcp6c -s ${pkgs.odhcp-script} -e -v -p /run/${name}.pid -P 48 $(output ${services.wan} ifname)
)
'';
dependencies = [ services.wan ];
};
services.acquire-lan-prefix =
let script = pkgs.callPackage ./acquire-delegated-prefix.nix { };
in longrun {
name = "acquire-lan-prefix";
run = "${script} /run/service-state/dhcp6c.wan ${services.int.device}";
dependencies = [ services.dhcp6 ];
};
services.acquire-wan-address =
let script = pkgs.callPackage ./acquire-wan-address.nix { };
in longrun {
name = "acquire-wan-address";
run = "${script} /run/service-state/dhcp6c.wan $(output ${services.wan} ifname)";
dependencies = [ services.dhcp6 ];
};
services.default = target {
name = "default";
contents = with config.services; [
config.hardware.networkInterfaces.lo
config.hardware.networkInterfaces.lan
int
bridge
hostap
hostap5
ntp
defaultroute4
defaultroute6
packet_forwarding
dns
resolvconf
sshd
config.services.hostname
dhcp6
acquire-lan-prefix
acquire-wan-address
];
};
defaultProfile.packages = with pkgs; [
min-collect-garbage
nftables
strace
tcpdump
s6
];
programs.busybox = {
applets = [
"fdisk" "sfdisk"
];
options = {
FEATURE_FANCY_TAIL = "y";
};
};
}

View File

@ -1,109 +0,0 @@
{ config, pkgs, lib, lim, ... } :
let
inherit (pkgs) serviceFns;
svc = config.system.service;
in rec {
imports = [
../modules/network
../modules/ssh
../modules/vlan
../modules/wlan.nix
../modules/hostapd
../modules/bridge
../modules/ext4fs.nix
../modules/tftpboot.nix
];
rootfsType = "ext4";
boot.tftp = {
# IP addresses to use in the boot monitor when flashing/ booting
# over TFTP. If you are flashing using the stock firmware's Web UI
# then these dummy values are fine
ipaddr = "10.0.0.8"; # my address
serverip = "10.0.0.1"; # build machine or other tftp server
loadAddress = lim.parseInt "0x40000800";
};
hostname = "omnia";
services.hostap =
let secrets = {
ssid = "not-the-internet";
channel = 4;
wpa_passphrase = "diamond dogs";
};
in svc.hostapd.build {
interface = config.hardware.networkInterfaces.wlan;
params = {
country_code = "GB";
hw_mode = "g";
wmm_enabled = 1;
ieee80211n = 1;
inherit (secrets) ssid channel wpa_passphrase;
auth_algs = 1; # 1=wpa2, 2=wep, 3=both
wpa = 2; # 1=wpa, 2=wpa2, 3=both
wpa_key_mgmt = "WPA-PSK";
wpa_pairwise = "TKIP CCMP"; # auth for wpa (may not need this?)
rsn_pairwise = "CCMP"; # auth for wpa2
};
};
services.hostap5 =
let secrets = {
ssid = "not-the-internet";
channel = 36;
wpa_passphrase = "diamond dogs";
};
in svc.hostapd.build {
interface = config.hardware.networkInterfaces.wlan5;
params = {
country_code = "GB";
hw_mode = "a";
ht_capab = "[HT40+]";
vht_oper_chwidth = 1;
vht_oper_centr_freq_seg0_idx = secrets.channel + 6;
ieee80211ac = 1;
wmm_enabled = 1;
inherit (secrets) ssid channel wpa_passphrase;
auth_algs = 1; # 1=wpa2, 2=wep, 3=both
wpa = 2; # 1=wpa, 2=wpa2, 3=both
wpa_key_mgmt = "WPA-PSK";
wpa_pairwise = "TKIP CCMP"; # auth for wpa (may not need this?)
rsn_pairwise = "CCMP"; # auth for wpa2
};
};
services.int = svc.bridge.primary.build {
ifname = "int";
};
services.dhcpc = svc.network.dhcp.client.build {
interface = services.int;
dependencies = [ config.services.hostname ];
};
services.bridge = svc.bridge.members.build {
primary = services.int;
members = with config.hardware.networkInterfaces; [
lan
wlan
];
};
services.sshd = svc.ssh.build { };
users.root = {
# the password is "secret". Use mkpasswd -m sha512crypt to
# create this hashed password string
passwd = "$6$y7WZ5hM6l5nriLmo$5AJlmzQZ6WA.7uBC7S8L4o19ESR28Dg25v64/vDvvCN01Ms9QoHeGByj8lGlJ4/b.dbwR9Hq2KXurSnLigt1W1";
};
defaultProfile.packages = with pkgs; [
figlet pciutils
];
}

48
kernel/uimage.nix Normal file
View File

@ -0,0 +1,48 @@
{
lzma
, stdenv
, ubootTools
, dtc
} :
let
objcopy = "${stdenv.cc.bintools.targetPrefix}objcopy";
in {
kernel
, commandLine
, entryPoint
, extraName ? "" # e.g. socFamily
, loadAddress
, dtb ? null
} :
stdenv.mkDerivation {
name = "kernel.image";
phases = [
"preparePhase"
(if dtb != null then "dtbPhase" else ":")
"buildPhase"
"installPhase" ];
nativeBuildInputs = [
lzma
dtc
stdenv.cc
ubootTools
];
preparePhase = ''
cp ${kernel} vmlinux.elf; chmod +w vmlinux.elf
'';
dtbPhase = ''
dtc -I dtb -O dts -o tmp.dts ${dtb}
echo '/{ chosen { bootargs = ${builtins.toJSON commandLine}; }; };' >> tmp.dts
dtc -I dts -O dtb -o tmp.dtb tmp.dts
${objcopy} --update-section .appended_dtb=tmp.dtb vmlinux.elf || ${objcopy} --add-section .appended_dtb=${dtb} vmlinux.elf
'';
buildPhase = ''
${objcopy} -O binary -R .reginfo -R .notes -R .note -R .comment -R .mdebug -R .note.gnu.build-id -S vmlinux.elf vmlinux.bin
rm -f vmlinux.bin.lzma ; lzma -k -z vmlinux.bin
mkimage -A mips -O linux -T kernel -C lzma -a ${loadAddress} -e ${entryPoint} -n 'MIPS Liminix Linux ${extraName}' -d vmlinux.bin.lzma kernel.uimage
'';
installPhase = ''
cp kernel.uimage $out
'';
}

View File

@ -1,40 +0,0 @@
# Import all of the modules, used in the documentation generator. Not
# currently expected to work in an actual configuration, but it would
# be nice if it did.
{
imports = [
./base.nix
./bridge
./busybox.nix
./dhcp6c
./dnsmasq
./firewall
./hardware.nix
./hostapd
./hostname.nix
./kernel
./mdevd.nix
./mount
./network
./ntp
./outputs.nix
./outputs/ext4fs.nix
./outputs/initramfs.nix
./outputs/jffs2.nix
./outputs/kexecboot.nix
./outputs/mtdimage.nix
./outputs/tftpboot.nix
./outputs/ubifs.nix
./outputs/ubimage.nix
./outputs/vmroot.nix
./ppp
./ramdisk.nix
./squashfs.nix
./ssh
./users.nix
./vlan
./watchdog
./wlan.nix
];
}

View File

@ -1,18 +0,0 @@
{ lib, lim, pkgs, config, ...}:
{
config = {
kernel.config = {
CPU_LITTLE_ENDIAN= "y";
CPU_BIG_ENDIAN= "n";
# CMDLINE_FROM_BOOTLOADER availability is conditional
# on CMDLINE being set to something non-empty
CMDLINE="\"empty=false\"";
CMDLINE_FROM_BOOTLOADER = "y";
OF = "y";
# USE_OF = "y";
};
hardware.ram.startAddress = lim.parseInt "0x40000000";
system.outputs.u-boot = pkgs.ubootQemuAarch64;
};
}

View File

@ -1,11 +0,0 @@
{ lib, lim, pkgs, config, ...}:
{
config = {
kernel.config = {
OF = "y";
};
kernel.makeTargets = ["arch/arm/boot/zImage"];
hardware.ram.startAddress = lim.parseInt "0x40000000";
system.outputs.u-boot = pkgs.ubootQemuArm;
};
}

View File

@ -1,18 +0,0 @@
{ lib, pkgs, config, lim, ...}:
{
config = {
kernel.config = {
MIPS_ELF_APPENDED_DTB = "y";
MIPS_BOOTLOADER_CMDLINE_REQUIRE_COOKIE = "y";
MIPS_BOOTLOADER_CMDLINE_COOKIE = "\"liminix\"";
MIPS_CMDLINE_DTB_EXTEND = "y";
OF = "y";
USE_OF = "y";
};
hardware.ram.startAddress = lim.parseInt "0x80000000";
boot.commandLine = [
"console=ttyS0,115200" # true of all mips we've yet encountered
];
};
}

View File

@ -1,10 +0,0 @@
{ lib, pkgs, config, ...}:
{
imports = [ ./mips.nix ];
config = {
kernel.config = {
CPU_BIG_ENDIAN = "y";
};
system.outputs.u-boot = pkgs.ubootQemuMips;
};
}

View File

@ -1,9 +0,0 @@
{ lib, pkgs, config, ...}:
{
imports = [ ./mips.nix ];
config = {
kernel.config = {
CPU_LITTLE_ENDIAN = "y";
};
};
}

View File

@ -1,7 +1,3 @@
## Base options
## ============
{ lib, pkgs, config, ...}:
let
inherit (lib) mkEnableOption mkOption types isDerivation hasAttr ;
@ -13,91 +9,49 @@ let
in {
options = {
# analogous to nixos systemPackages, but we don't symlink into
# /run/current-system, we just add the paths in /etc/profile
defaultProfile = {
packages = mkOption {
type = types.listOf types.package;
description = ''
List of packages which are available in a login shell. (This
is analogous to systemPackages in NixOS, but we don't symlink into
/run/current-system, we just add the paths in /etc/profile
'';
};
};
services = mkOption {
type = types.attrsOf type_service;
};
system.callService = mkOption {
type = types.functionTo (types.functionTo types.anything);
};
filesystem = mkOption {
type = types.anything;
description = ''
Skeleton filesystem, represented as nested attrset. Consult the
source code if you need to add to this
'';
# internal = true; # probably a good case to make this internal
};
filesystem = mkOption { type = types.anything; };
rootfsType = mkOption {
default = "squashfs";
type = types.enum [
"btrfs"
"ext4"
"jffs2"
"squashfs"
"ubifs"
];
type = types.str;
};
rootOptions = mkOption {
type = types.nullOr types.str;
default = null;
kernel = {
src = mkOption { type = types.package; } ;
modular = mkOption {
type = types.bool;
default = true;
description = "support loadable kernel modules";
};
extraPatchPhase = mkOption {
default = "true";
type = types.lines;
} ;
config = mkOption {
# mostly the values are y n or m, but sometimes
# other strings are also used
type = types.attrsOf types.nonEmptyStr;
};
};
boot = {
commandLine = mkOption {
type = types.listOf types.nonEmptyStr;
default = [];
description = "Kernel command line";
};
commandLineDtbNode = mkOption {
type = types.enum [ "bootargs" "bootargs-override" ];
default = "bootargs";
description = "Kernel command line's devicetree node";
};
imageType = mkOption {
type = types.enum [ "primary" "secondary" ];
default = "primary";
};
imageFormat = mkOption {
type = types.enum ["fit" "uimage"];
default = "uimage";
};
tftp = {
loadAddress = mkOption {
type = types.ints.unsigned;
description = ''
RAM address at which to load data when transferring via
TFTP. This is not the address of the flash storage,
nor the kernel load address: it should be set to some part
of RAM that's not used for anything else and suitable for
temporary storage.
'';
};
loadAddress = mkOption { type = types.str; };
# These names match the uboot environment variables. I reserve
# the right to change them if I think of better ones.
ipaddr = mkOption {
type = types.str;
description = ''
Our IP address to use when creating scripts to
boot or flash from U-Boot. Not relevant in normal operation
'';
};
serverip = mkOption {
type = types.str;
description = ''
IP address of the TFTP server. Not relevant in normal operation
'';
};
ipaddr = mkOption { type = types.str; };
serverip = mkOption { type = types.str; };
};
};
};
@ -105,38 +59,70 @@ in {
defaultProfile.packages = with pkgs;
[ s6 s6-init-bin execline s6-linux-init s6-rc ];
hardware.networkInterfaces = {
lo =
let iface = interface { type = "loopback"; device = "lo";};
in bundle {
name = "loopback";
contents = [
(address iface { family = "inet4"; address ="127.0.0.1"; prefixLength = 8;})
(address iface { family = "inet6"; address ="::1"; prefixLength = 128;})
];
};
};
kernel = rec {
modular = true; # disabling this is not yet supported
config = {
IKCONFIG = "y";
IKCONFIG_PROC = "y";
PROC_FS = "y";
KEXEC = "y";
MODULES = if modular then "y" else "n";
MODULE_SIG = if modular then "y" else "n";
DEBUG_FS = "y";
MIPS_BOOTLOADER_CMDLINE_REQUIRE_COOKIE = "y";
MIPS_BOOTLOADER_CMDLINE_COOKIE = "\"liminix\"";
MIPS_CMDLINE_DTB_EXTEND = "y";
# basic networking protocols
NET = "y";
UNIX = "y";
INET = "y";
IPV6 = "y";
PACKET = "y"; # for ppp, tcpdump ...
SYSVIPC= "y";
# disabling this option causes the kernel to use an "empty"
# initramfs instead: it has a /dev/console node and not much
# else. Note that pid 1 is started *before* the root
# filesystem is mounted and it expects /dev/console to be
# present already
BLK_DEV_INITRD = lib.mkDefault "n"; # overriden by initramfs module
# s6-linux-init mounts this on /dev
DEVTMPFS = "y";
# some or all of these may be fix for "tmpfs: Unknown parameter 'mode'" error
TMPFS = "y";
TMPFS_POSIX_ACL = "y";
TMPFS_XATTR = "y";
FW_LOADER = "y";
FW_LOADER_COMPRESS = "y";
# We don't have a user helper, so we get multiple 60s pauses
# at boot time unless we disable trying to call it.
# https://lkml.org/lkml/2013/8/5/175
FW_LOADER_USER_HELPER = "n";
};
};
boot.commandLine = [
"panic=10 oops=panic init=/bin/init loglevel=8"
"console=ttyS0,115200 panic=10 oops=panic init=/bin/init loglevel=8"
"root=${config.hardware.rootDevice}"
"rootfstype=${config.rootfsType}"
"fw_devlink=off"
] ++ lib.optional (config.rootOptions != null) "rootflags=${config.rootOptions}";
system.callService = path : parameters :
let
typeChecked = caller: type: value:
let
inherit (lib) types mergeDefinitions;
defs = [{ file = caller; inherit value; }];
type' = types.submodule { options = type; };
in (mergeDefinitions [] type' defs).mergedValue;
cp = lib.callPackageWith(pkgs // { svc = config.system.service; });
pkg = cp path {};
checkTypes = t : p : typeChecked (builtins.toString path) t p;
in {
inherit parameters;
build = { dependencies ? [], ... } @ args :
let
s = pkg (checkTypes parameters
(builtins.removeAttrs args ["dependencies"]));
in s.overrideAttrs (o: {
dependencies = (builtins.map (d: d.name) dependencies) ++ o.dependencies;
buildInputs = dependencies ++ o.buildInputs;
});
};
];
users.root = {
uid = 0; gid= 0; gecos = "Root of all evaluation";
dir = "/home/root/";
@ -176,7 +162,6 @@ in {
proc = dir {};
run = dir {};
sys = dir {};
tmp = dir {};
};
};
}

View File

@ -4,7 +4,9 @@
## Allows creation of Layer 2 software "bridge" network devices. A
## common use case is to merge together a hardware Ethernet device
## with one or more WLANs so that several local devices appear to be
## on the same network.
## on the same network. Create a ``primary`` service to specify the
## new device, and a ``members`` service to add constituent devices
## to it.
{ lib, pkgs, config, ...}:
@ -14,40 +16,33 @@ let
inherit (pkgs) liminix;
in
{
imports = [ ../ifwait ];
options = {
system.service.bridge = {
primary = mkOption { type = liminix.lib.types.serviceDefn; };
members = mkOption { type = liminix.lib.types.serviceDefn; };
primary = mkOption {
type = liminix.lib.types.serviceDefn;
};
members = mkOption {
type = liminix.lib.types.serviceDefn;
};
};
};
config.system.service.bridge = {
primary = liminix.callService ./primary.nix {
ifname = mkOption {
type = types.str;
description = "bridge interface name to create";
description = "interface name for the bridge device";
};
};
members = config.system.callService ./members.nix {
primary = mkOption {
type = liminix.lib.types.interface;
description = "primary bridge interface";
};
members = liminix.callService ./members.nix {
members = mkOption {
type = types.listOf liminix.lib.types.interface;
type = types.listOf liminix.lib.types.service;
description = "interfaces to add to the bridge";
};
primary = mkOption {
type = liminix.lib.types.service;
description = "bridge interface to add them to";
};
};
};
config.kernel.config = {
BRIDGE = "y";
BRIDGE_IGMP_SNOOPING = "y";
} // lib.optionalAttrs (config.system.service ? vlan) {
# depends on bridge _and_ vlan. I would like there to be
# a better way to test for the existence of vlan config:
# maybe the module should set an `enabled` attribute?
BRIDGE_VLAN_FILTERING = "y";
};
config.kernel.config.BRIDGE = "y";
}

View File

@ -2,31 +2,20 @@
liminix
, ifwait
, lib
, svc
}:
{ members, primary } :
let
inherit (liminix.networking) interface;
inherit (liminix.services) bundle oneshot;
inherit (lib) mkOption types;
addif = member :
# how do we get sight of services from here? maybe we need to
# implement ifwait as a regualr derivation instead of a
# servicedefinition
svc.ifwait.build {
state = "running";
interface = member;
oneshot {
name = "add-${member.device}-to-br-${primary.device}";
up = "${ifwait}/bin/ifwait ${member.device} running && ip link set dev ${member.device} master ${primary.device}";
down = "ip link set dev ${member.device} nomaster";
dependencies = [ primary member ];
service = oneshot {
name = "${primary.name}.member.${member.name}";
up = ''
ip link set dev $(output ${member} ifname) master $(output ${primary} ifname)
'';
down = "ip link set dev $(output ${member} ifname) nomaster";
};
};
in bundle {
name = "${primary.name}.members";
name = "bridge-${primary.device}-members";
contents = map addif members;
}

View File

@ -1,17 +1,13 @@
{
liminix
, ifwait
, lib
}:
{ ifname } :
let
inherit (liminix.services) bundle oneshot;
inherit (liminix.networking) interface;
inherit (liminix.lib) typeChecked;
inherit (lib) mkOption types;
in oneshot rec {
name = "${ifname}.link";
up = ''
ip link add name ${ifname} type bridge
${liminix.networking.ifup name ifname}
'';
down = "ip link set down dev ${ifname}";
in interface {
device = ifname;
type = "bridge";
}

View File

@ -1,11 +1,3 @@
## Busybox
## =======
##
## Busybox provides stripped-down versions of many usual
## Linux/Unix tools, and may be configured to include only
## the commands (termed "applets") required by the user or
## by other included modules.
{ lib, pkgs, config, ...}:
let
inherit (lib) mkOption mkEnableOption types mapAttrsToList;
@ -32,38 +24,37 @@ let
(a: symlink "${busybox}/bin/busybox");
minimalApplets = [
# this is probably less minimal than it could be
"arch" "ash" "base64" "basename" "bc" "brctl" "bunzip2" "bzcat" "bzip2"
"cal" "cat" "chattr" "chgrp" "chmod" "chown" "chpst" "chroot" "clear" "cmp"
"comm" "cp" "cpio" "cut" "date" "dhcprelay" "dd" "df" "dirname" "dmesg"
"du" "echo" "egrep" "env" "expand" "expr" "false" "fdisk" "fgrep" "find"
"free" "fuser" "grep" "gunzip" "gzip" "head" "hexdump" "hostname" "hwclock"
"ifconfig" "ip" "ipaddr" "iplink" "ipneigh" "iproute" "iprule" "kill"
"killall" "killall5" "less" "ln" "ls" "lsattr" "lsof" "md5sum" "mkdir"
"mknod" "mktemp" "mount" "mv" "nc" "netstat" "nohup" "od" "pgrep" "pidof"
"ping" "ping6" "pkill" "pmap" "printenv" "printf" "ps" "pwd" "readlink"
"realpath" "reset" "rm" "rmdir" "route" "sed" "seq" "setsid" "sha1sum"
"sha256sum" "sha512sum" "sleep" "sort" "stat" "strings" "stty" "su" "sum"
"swapoff" "swapon" "sync" "tail" "tee" "test" "time" "touch" "tr"
"traceroute" "traceroute6" "true" "truncate" "tty" "udhcpc" "umount"
"uname" "unexpand" "uniq" "unlink" "unlzma" "unxz" "unzip" "uptime" "watch"
"wc" "whoami" "xargs" "xxd" "xz" "xzcat" "yes" "zcat"
"arch" "ash" "base64" "basename" "bc" "brctl" "bunzip2" "bzcat"
"bzip2" "cal" "cat" "chattr" "chgrp" "chmod" "chown" "chpst"
"chroot" "clear" "cmp" "comm" "cp" "cpio" "cut" "date" "dd" "df"
"dirname" "dmesg" "du" "echo" "egrep" "env" "expand" "expr"
"false" "fdisk" "fgrep" "find" "free" "fuser" "grep" "gunzip"
"gzip" "head" "hexdump" "hostname" "hwclock" "ifconfig" "ip"
"ipaddr" "iplink" "ipneigh" "iproute" "iprule" "kill" "killall"
"killall5" "less" "ln" "ls" "lsattr" "lsof" "md5sum" "mkdir"
"mknod" "mktemp" "mount" "mv" "nc" "netstat" "nohup" "od" "pgrep"
"pidof" "ping" "ping6" "pkill" "pmap" "printenv" "printf" "ps"
"pwd" "readlink" "realpath" "reset" "rm" "rmdir" "route" "sed"
"seq" "setsid" "sha1sum" "sha256sum" "sha512sum" "sleep" "sort"
"stat" "strings" "stty" "su" "sum" "swapoff" "swapon" "sync"
"tail" "tee" "test" "time" "touch" "tr" "traceroute" "traceroute6"
"true" "truncate" "tty" "udhcpc" "umount" "uname"
"unexpand" "uniq" "unlink" "unlzma" "unxz" "unzip" "uptime"
"watch" "wc" "whoami" "xargs" "xxd" "xz" "xzcat" "yes" "zcat"
];
in {
options = {
programs.busybox = {
applets = mkOption {
type = types.listOf types.str;
description = "Applets required";
default = [];
example = ["sh" "getty" "login"];
};
options = mkOption {
# mostly the values are y n or m, but sometimes
# other strings are also used
description = "Other busybox config flags that do not map directly to applet names (often prefixed FEATURE_)";
type = types.attrsOf types.nonEmptyStr;
default = { };
example = { FEATURE_DD_IBS_OBS = "y"; };
};
};
};

View File

@ -1,32 +0,0 @@
(local { : system } (require :anoia))
(local svc (require :anoia.svc))
(fn deletions [old-addresses new-addresses]
(let [deleted {}]
(each [n address (pairs old-addresses)]
(let [now (. new-addresses n)]
(if (or (not now) (not (= now.len address.len)))
(table.insert deleted address))))
deleted))
(fn update-prefixes [wan-device addresses new-addresses exec]
(each [_ p (ipairs (deletions addresses new-addresses))]
(exec
(.. "ip address del " p.address "1/" p.len " dev " wan-device)))
(each [_ p (pairs new-addresses)]
(exec
(.. "ip address change " p.address "1/" p.len
" dev " wan-device
" valid_lft " p.valid
" preferred_lft " p.preferred
)))
new-addresses)
(fn run []
(let [[state-directory lan-device] arg
dir (svc.open state-directory)]
(accumulate [addresses []
v (dir:events)]
(update-prefixes lan-device addresses (v:output "prefix") system))))
{ : changes : run }

View File

@ -1,11 +0,0 @@
{
writeFennel
, linotify
, anoia
, lua
, lualinux
}:
writeFennel "acquire-delegated-prefix" {
packages = [ linotify anoia lualinux ];
mainFunction = "run";
} ./acquire-delegated-prefix.fnl

View File

@ -1,124 +0,0 @@
(local subject (require :acquire-wan-address))
(import-macros { : expect= } :anoia.assert)
(local { : merge : dup } (require :anoia))
;; nix-shell --run "cd modules/dhcp6c && fennelrepl acquire-wan-address-test.fnl"
(local a1
{
"2001-ab-cd-ef" {
:address "2001:ab:cd:ef"
:len "64"
:preferred "3600"
:valid "7200"
}
}
)
(local a156
{
"2001-ab-cd-ef" {
:address "2001:ab:cd:ef"
:len "56"
:preferred "3600"
:valid "7200"
}
}
)
(local a2
{
"2001-0-1-2-3" {
:address "2001:0:1:2:3"
:len "64"
:preferred "3600"
:valid "7200"
}
}
)
(local a21
{
"2001-0-1-2-3" {
:address "2001:0:1:2:3"
:len "64"
:preferred "1800"
:valid "5400"
}
}
)
(fn first-address []
(let [deleted
(subject.deletions
{ }
a1
)]
(expect= deleted [])))
(fn second-address []
(let [del
(subject.deletions
a1
(merge (dup a1) a2)
)]
(expect= del [])))
(fn old-address-is-deleted []
(let [del
(subject.deletions
(merge (dup a1) a2)
a1
)]
(expect= (. del 1) (. a2 "2001-0-1-2-3"))
))
(fn changed-lifetime-not-deleted []
(let [del
(subject.deletions
(merge (dup a1) a2)
(merge (dup a1) a21)
)]
;; when an address lifetime changes, "ip address change"
;; will update that so it need not (should not) be deleted
(expect= del [])))
(fn changed-prefix-is-deleted []
(let [del
(subject.deletions a1 a156)]
;; when an address prefix changes, "ip address change"
;; ignores that cjhange, so we have to remove the
;; address before reinstating it
(expect= del [(. a1 "2001-ab-cd-ef")])))
(first-address)
(second-address)
(old-address-is-deleted)
(changed-lifetime-not-deleted)
(changed-prefix-is-deleted)
(let [cmds []]
(subject.update-addresses
"ppp0" a1 (merge (dup a1) a2)
(fn [a] (table.insert cmds a)))
(expect=
(doto cmds table.sort)
[
;; order of changes is unimportant
"ip address change 2001:0:1:2:3/64 dev ppp0 valid_lft 7200 preferred_lft 3600"
"ip address change 2001:ab:cd:ef/64 dev ppp0 valid_lft 7200 preferred_lft 3600"
]))
(let [cmds []]
(subject.update-addresses
"ppp0" (merge (dup a1) a2) a1
(fn [a] (table.insert cmds a)))
(expect=
cmds
[
;; deletes are executed before changes
"ip address del 2001:0:1:2:3/64 dev ppp0"
"ip address change 2001:ab:cd:ef/64 dev ppp0 valid_lft 7200 preferred_lft 3600"
]))
(print "OK")

View File

@ -1,32 +0,0 @@
(local { : system } (require :anoia))
(local svc (require :anoia.svc))
(fn deletions [old-addresses new-addresses]
(let [deleted {}]
(each [n address (pairs old-addresses)]
(let [now (. new-addresses n)]
(if (or (not now) (not (= now.len address.len)))
(table.insert deleted address))))
deleted))
(fn update-addresses [wan-device addresses new-addresses exec]
(each [_ p (ipairs (deletions addresses new-addresses))]
(exec
(.. "ip address del " p.address "/" p.len " dev " wan-device)))
(each [_ p (pairs new-addresses)]
(exec
(.. "ip address change " p.address "/" p.len
" dev " wan-device
" valid_lft " p.valid
" preferred_lft " p.preferred
)))
new-addresses)
(fn run []
(let [[state-directory wan-device] arg
dir (svc.open state-directory)]
(accumulate [addresses []
v (dir:events)]
(update-addresses wan-device addresses (v:output "address") system))))
{ : update-addresses : deletions : run }

View File

@ -1,11 +0,0 @@
{
writeFennel
, linotify
, anoia
, lualinux
, lua
}:
writeFennel "acquire-wan-address" {
packages = [ linotify anoia lualinux ];
mainFunction = "run";
} ./acquire-wan-address.fnl

View File

@ -1,16 +0,0 @@
{
liminix
, lib
, callPackage
}:
{ client, interface } :
let
inherit (liminix.services) longrun;
inherit (lib) mkOption types;
name = "dhcp6c.addr.${client.name}.${interface.name}";
script = callPackage ./acquire-wan-address.nix { };
in longrun {
inherit name;
run = "${script} $SERVICE_OUTPUTS/${client.name} $(output ${interface} ifname)";
dependencies = [ client interface ];
}

View File

@ -1,21 +0,0 @@
{
liminix
, lib
, odhcp6c
, odhcp-script
}:
{ interface } :
let
inherit (liminix.services) longrun;
inherit (lib) mkOption types;
name = "dhcp6c.${interface.name}";
in longrun {
inherit name;
notification-fd = 10;
run = ''
export SERVICE_STATE=$SERVICE_OUTPUTS/${name}
${odhcp6c}/bin/odhcp6c -s ${odhcp-script} -e -v -p /run/${name}.pid -P0 $(output ${interface} ifname)
)
'';
dependencies = [ interface ];
}

View File

@ -1,52 +0,0 @@
## DHCP6 client module
## ===================
##
## This is for use if you have an IPv6-capable upstream that provides
## address information and/or prefix delegation using DHCP6. It
## provides a service to request address information in the form of a
## DHCP lease, and two dependent services that listen for updates
## to the DHCP address information and can be used to update
## addresses of network interfaces that you want to assign those
## prefixes to
{ lib, pkgs, config, ...}:
let
inherit (lib) mkOption types;
inherit (pkgs.liminix.services) oneshot;
inherit (pkgs) liminix;
in
{
options = {
system.service.dhcp6c = {
client = mkOption { type = liminix.lib.types.serviceDefn; };
prefix = mkOption { type = liminix.lib.types.serviceDefn; };
address = mkOption { type = liminix.lib.types.serviceDefn; };
};
};
config.system.service.dhcp6c = {
client = liminix.callService ./client.nix {
interface = mkOption {
type = liminix.lib.types.interface;
description = "interface (usually WAN) to query for DHCP6";
};
};
address = liminix.callService ./address.nix {
client = mkOption {
type = types.anything; # liminix.lib.types.service;
};
interface = mkOption {
type = liminix.lib.types.interface;
description = "interface to assign the address to";
};
};
prefix = liminix.callService ./prefix.nix {
client = mkOption {
type = types.anything; # liminix.lib.types.service;
};
interface = mkOption {
type = liminix.lib.types.interface;
description = "interface to assign <prefix>::1 to";
};
};
};
}

View File

@ -1,16 +0,0 @@
{
liminix
, lib
, callPackage
}:
{ client, interface } :
let
inherit (liminix.services) longrun;
inherit (lib) mkOption types;
name = "dhcp6c.prefix.${client.name}.${interface.name}";
script = callPackage ./acquire-delegated-prefix.nix { };
in longrun {
inherit name;
run = "${script} $SERVICE_OUTPUTS/${client.name} $(output ${interface} ifname)";
dependencies = [ client interface ];
}

View File

@ -20,12 +20,10 @@ in {
user = mkOption {
type = types.str;
default = "dnsmasq";
description = "Specifies the unix user which dnsmasq will run as" ;
};
group = mkOption {
type = types.str;
default = "dnsmasq";
description = "Specifies the unix group which dnsmasq will run as" ;
};
resolvconf = mkOption {
type = types.nullOr liminix.lib.types.service;
@ -42,38 +40,6 @@ in {
ranges = mkOption {
type = types.listOf types.str;
};
hosts = mkOption {
default = {};
type = types.attrsOf (types.submodule {
options = {
mac = mkOption {
description = ''
MAC or other hardware address to match on. For Ethernet
this is a 48 bit address represented as colon-separated
hex bytes, or "id:clientid" to match a presented
client id (IPv6 DUID)
'';
type = types.str;
example = "01:20:31:4a:50";
};
v4 = mkOption {
description = "IPv4 address to assign to this client";
example = "192.0.2.1";
type = types.str;
};
v6 = mkOption {
type = types.listOf types.str;
description = "IPv6 addresses or interface-ids to assign to this client";
default = [];
example = [ "fe80::42:1eff:fefd:b341" "::1234"];
};
leasetime = mkOption {
type = types.int;
default = 86400;
};
};
});
};
domain = mkOption {
# this can be given multiple times so probably should be
# domains plural and list of string

View File

@ -10,17 +10,13 @@
, domain
, group
, ranges
, hosts
, upstreams
, resolvconf
}:
let
name = "${interface.name}.dnsmasq";
name = "${interface.device}.dnsmasq";
inherit (liminix.services) longrun;
inherit (lib) concatStrings concatStringsSep mapAttrsToList;
hostOpt = name : { mac, v4, v6, leasetime } @ attrs:
let v6s = concatStrings (map (a : ",[${a}]") v6);
in "--dhcp-host=${mac},${v4}${v6s},${name},${builtins.toString leasetime}";
inherit (lib) concatStringsSep;
in
longrun {
inherit name;
@ -31,21 +27,19 @@ longrun {
--user=${user} \
--domain=${domain} \
--group=${group} \
--interface=$(output ${interface} ifname) \
--interface=${interface.device} \
${lib.concatStringsSep " " (builtins.map (r: "--dhcp-range=${r}") ranges)} \
${lib.concatStringsSep " " (builtins.map (r: "--server=${r}") upstreams)} \
--keep-in-foreground \
--dhcp-authoritative \
${if resolvconf != null then "--resolv-file=$(output_path ${resolvconf} resolv.conf)" else "--no-resolv"} \
${lib.concatStringsSep " " (mapAttrsToList hostOpt hosts)} \
--no-hosts \
--log-dhcp \
--enable-ra \
--log-debug \
--log-queries \
--log-facility=- \
--dhcp-leasefile=$(mkstate ${name})/leases \
--dhcp-leasefile=/run/${name}.leases \
--pid-file=/run/${name}.pid
'';
# --log-debug \
# --log-queries \
}

View File

@ -10,41 +10,34 @@ let
inherit (pkgs) liminix;
inherit (pkgs.liminix.services) oneshot;
kmodules = pkgs.kmodloader.override {
inherit (config.system.outputs) kernel;
kconf = isModule :
# setting isModule false is utterly untested and mostly
# unimplemented: I say this to preempt any "how on earth is this
# even supposed to work?" questions
let yes = if isModule then "m" else "y";
in {
NFT_FIB_IPV4 = yes;
NFT_FIB_IPV6 = yes;
NF_TABLES = yes;
NF_CT_PROTO_DCCP = "y";
NF_CT_PROTO_SCTP = "y";
NF_CT_PROTO_UDPLITE = "y";
# NF_CONNTRACK_FTP = yes;
NFT_CT = yes;
};
kmodules = pkgs.kernel-modules.override {
kernelSrc = config.system.outputs.kernel.src;
modulesupport = config.system.outputs.kernel.modulesupport;
targets = [
"nft_fib_ipv4"
"nft_fib_ipv6"
"nf_log_syslog"
"ip6_tables"
"ip_tables"
"iptable_nat"
"nf_conntrack"
"nf_defrag_ipv4"
"nf_defrag_ipv6"
"nf_log_syslog"
"nf_nat"
"nf_reject_ipv4"
"nf_reject_ipv6"
"nf_tables"
"nft_chain_nat"
"nft_ct"
"nft_fib"
"nft_fib_ipv4"
"nft_fib_ipv6"
"nft_log"
"nft_masq"
"nft_nat"
"nft_reject"
"nft_reject_inet"
"nft_reject_ipv4"
"nft_reject_ipv6"
"x_tables"
"xt_MASQUERADE"
"xt_nat"
"xt_tcpudp"
];
kconfig = kconf true;
};
loadModules = oneshot {
name = "firewall-modules";
up = "sh ${kmodules}/load.sh";
down = "sh ${kmodules}/unload.sh";
};
in
{
@ -56,56 +49,44 @@ in
config = {
system.service.firewall =
let svc = liminix.callService ./service.nix {
extraRules = mkOption {
type = types.attrsOf types.attrs;
description = "firewall ruleset";
default = {};
};
rules = mkOption {
ruleset = mkOption {
type = types.attrsOf types.attrs; # we could usefully tighten this a bit :-)
default = import ./default-rules.nix;
description = "firewall ruleset";
};
};
in svc // {
build = args :
let args' = args // {
dependencies = (args.dependencies or []) ++ [kmodules];
};
in svc.build args' ;
build = args : (svc.build args) // {
dependencies = [ loadModules ] ++ (svc.dependencies or []);
};
};
programs.busybox.applets = [
"insmod" "rmmod"
];
# For historical reasons the kernel config is split between
# monolithic options and modules. TODO: go through this list
# and see what can be moved into the "kconf" definiton above
kernel.config = {
NETFILTER_XT_MATCH_CONNTRACK = "y";
IP6_NF_IPTABLES= "y";
IP_NF_IPTABLES= "y";
IP_NF_NAT = "y";
IP_NF_TARGET_MASQUERADE = "y";
NETFILTER = "y";
NETFILTER_ADVANCED = "y";
NETFILTER_NETLINK = "m";
NF_CONNTRACK = "m";
NETFILTER_XTABLES = "y";
NETLINK_DIAG = "y";
NFT_COMPAT = "y";
NFT_CT = "y";
NFT_LOG = "y";
NFT_MASQ = "y";
NFT_NAT = "y";
NFT_REJECT = "y";
NFT_REJECT_INET = "y";
IP6_NF_IPTABLES= "m";
IP_NF_IPTABLES = "m";
IP_NF_NAT = "m";
IP_NF_TARGET_MASQUERADE = "m";
NFT_CT = "m";
NFT_FIB_IPV4 = "m";
NFT_FIB_IPV6 = "m";
NFT_LOG = "m";
NFT_MASQ = "m";
NFT_NAT = "m";
NFT_REJECT = "m";
NFT_REJECT_INET = "m";
NF_CT_PROTO_DCCP = "y";
NF_CT_PROTO_SCTP = "y";
NF_CT_PROTO_UDPLITE = "y";
NF_LOG_SYSLOG = "m";
NF_NAT = "m";
NF_NAT_MASQUERADE = "y";
NF_TABLES = "m";
NF_CONNTRACK = "y";
NF_NAT = "y";
NF_NAT_MASQUERADE = "y";
NF_TABLES= "y";
NF_TABLES_INET = "y";
NF_TABLES_IPV4 = "y";
NF_TABLES_IPV6 = "y";

View File

@ -4,12 +4,12 @@
, firewallgen
, nftables
}:
{ rules, extraRules }:
{ ruleset }:
let
inherit (liminix.services) oneshot;
inherit (liminix.lib) typeChecked;
inherit (lib) mkOption types;
script = firewallgen "firewall.nft" (lib.recursiveUpdate rules extraRules);
script = firewallgen "firewall.nft" ruleset;
in oneshot {
name = "firewall";
up = script;

88
modules/flashimage.nix Normal file
View File

@ -0,0 +1,88 @@
{
config
, pkgs
, lib
, ...
}:
let
inherit (lib) mkOption types concatStringsSep;
inherit (config.boot) tftp;
in {
options.system.outputs = {
firmware = mkOption {
type = types.package;
internal = true; # component of flashimage
description = ''
Binary image (combining kernel, FDT, rootfs, initramfs
if needed, etc) for the target device.
'';
};
flash-scr = mkOption {
type = types.package;
internal = true; # component of flashimage
description = ''
Copy-pastable U-Boot commands to TFTP download the
image and write it to flash
'';
};
flashimage = mkOption {
type = types.package;
description = ''
Flashable image for the target device, and the script to
install it
'';
};
};
config = {
kernel = {
config = {
MTD_SPLIT_UIMAGE_FW = "y";
};
};
programs.busybox.applets = [
"flashcp"
];
system.outputs = {
firmware =
let o = config.system.outputs; in
pkgs.runCommand "firmware" {} ''
dd if=${o.uimage} of=$out bs=128k conv=sync
dd if=${o.rootfs} of=$out bs=128k conv=sync,nocreat,notrunc oflag=append
'';
flashimage =
let o = config.system.outputs; in
pkgs.runCommand "flashimage" {} ''
mkdir $out
cd $out
ln -s ${o.firmware} firmware.bin
ln -s ${o.rootfs} rootfs
ln -s ${o.kernel} vmlinux
ln -s ${o.manifest} manifest
ln -s ${o.kernel.headers} build
ln -s ${o.uimage} uimage
ln -s ${o.dtb} dtb
ln -s ${o.flash-scr} flash.scr
'';
flash-scr =
let
inherit (pkgs.lib.trivial) toHexString;
inherit (config.hardware) flash;
in
pkgs.buildPackages.runCommand "" {} ''
imageSize=$(stat -L -c %s ${config.system.outputs.firmware})
cat > $out << EOF
setenv serverip ${tftp.serverip}
setenv ipaddr ${tftp.ipaddr}
tftp 0x$(printf %x ${tftp.loadAddress}) result/firmware.bin
erase 0x$(printf %x ${flash.address}) +${flash.size}
cp.b 0x$(printf %x ${tftp.loadAddress}) 0x$(printf %x ${flash.address}) \''${filesize}
echo command line was ${builtins.toJSON (concatStringsSep " " config.boot.commandLine)}
EOF
'';
};
};
}

View File

@ -1,11 +1,3 @@
## Hardware-dependent options
## ==========================
##
## These are attributes of the hardware not of the application
## you want to run on it, and would usually be set in the "device" file:
## :file:`devices/manuf-model/default.nix`
{ lib, pkgs, config, ...}:
let
inherit (lib) mkEnableOption mkOption types isDerivation hasAttr ;
@ -16,14 +8,8 @@ in {
hardware = {
dts = {
src = mkOption {
type = types.nullOr types.path;
description = ''
If the device requires an external device tree to be loaded
alongside the kernel, this is the path to the device tree source
(we usually get these from OpenWrt). This value may be null if the
platform creates the device tree - currently this is the case
only for QEMU.
'';
type = types.path;
description = "Path to the device tree source (usually from OpenWrt)";
};
includes = mkOption {
default = [];
@ -32,15 +18,9 @@ in {
};
};
defaultOutput = mkOption {
description = "\"Default\" output: what gets built for this device when outputs.default is requested. Typically this is \"mtdimage\" or \"vmroot\"";
description = "\"Default\" output: what gets built for this device when outputs.default is requested. Typically this is \"flashimage\" or \"vmroot\"";
type = types.nonEmptyStr;
};
ram = {
startAddress = mkOption {
type = types.int;
};
};
flash = {
# start address and size of whichever partition (often
# called "firmware") we're going to overwrite with our
@ -54,20 +34,19 @@ in {
kernel uimage and root fs. Usually not the entire flash, as
we don't want to clobber the bootloader, calibration data etc
'';
type = types.ints.unsigned;
type = types.str;
};
size = mkOption {
type = types.ints.unsigned;
type = types.str;
description = "Size in bytes of the firmware partition";
};
eraseBlockSize = mkOption {
description = "Flash erase block size in bytes";
type = types.ints.unsigned;
type = types.str;
};
};
loadAddress = mkOption { type = types.ints.unsigned; default = null; };
entryPoint = mkOption { type = types.ints.unsigned; };
alignment = mkOption { type = types.nullOr types.ints.unsigned; default = null; description = "Alignment passed to `mkimage` for FIT"; };
loadAddress = mkOption { default = null; };
entryPoint = mkOption { };
radios = mkOption {
description = ''
Kernel modules (from mac80211 package) required for the
@ -77,11 +56,7 @@ in {
default = [];
example = ["ath9k" "ath10k"];
};
rootDevice = mkOption {
description = "Full path to preferred root device";
type = types.str;
example = "/dev/mtdblock3";
};
rootDevice = mkOption { };
networkInterfaces = mkOption {
type = types.attrsOf types.anything;
};

View File

@ -17,7 +17,7 @@ let
# we'll add them as top-level attributes and rename params to
# extraParams
name = "${interface.name}.hostapd";
name = "${interface.device}.hostapd";
defaults = {
driver = "nl80211";
logger_syslog = "-1";
@ -35,5 +35,5 @@ let
in longrun {
inherit name;
dependencies = [ interface ];
run = "${hostapd}/bin/hostapd -i $(output ${interface} ifname) -P /run/${name}.pid -S ${conf}";
run = "${hostapd}/bin/hostapd -i ${interface.device} -P /run/${name}.pid -S ${conf}";
}

View File

@ -5,10 +5,6 @@ let
in {
options = {
hostname = mkOption {
description = ''
System hostname of the device, as returned by gethostname(2). May or
may not correspond to any name it's reachable at on any network.
'';
default = "liminix";
type = types.nonEmptyStr;
};

View File

@ -1,18 +0,0 @@
{ config, pkgs, lib, ... } :
let
inherit (pkgs) liminix;
inherit (lib) mkOption types;
in {
options.system.service.ifwait =
mkOption { type = liminix.lib.types.serviceDefn; };
config.system.service.ifwait = config.system.callService ./ifwait.nix {
state = mkOption { type = types.str; };
interface = mkOption {
type = liminix.lib.types.interface;
};
service = mkOption {
type = liminix.lib.types.service;
};
};
}

View File

@ -1,16 +0,0 @@
{ ifwait, liminix } :
{
state
, interface
, service
}:
let
inherit (liminix.services) longrun;
in longrun {
name = "ifwait.${interface.name}";
buildInputs = [ service ];
isTrigger = true;
run = ''
${ifwait}/bin/ifwait -s ${service.name} $(output ${interface} ifname) ${state}
'';
}

View File

@ -13,23 +13,13 @@ in
boot.initramfs = {
enable = mkEnableOption "initramfs";
};
system.outputs = {
initramfs = mkOption {
type = types.package;
internal = true;
description = ''
Initramfs image capable of mounting the real root
filesystem
'';
};
systemConfiguration = mkOption {
type = types.package;
description = ''
pkgs.systemconfig for the configured filesystem,
contains 'activate' and 'init' commands
'';
internal = true;
};
system.outputs.initramfs = mkOption {
type = types.package;
internal = true;
description = ''
Initramfs image capable of mounting the jffs2 root
filesystem
'';
};
};
config = mkIf config.boot.initramfs.enable {
@ -47,14 +37,18 @@ in
dir /proc 0755 0 0
dir /dev 0755 0 0
nod /dev/console 0600 0 0 c 5 1
nod /dev/mtdblock0 0600 0 0 b 31 0
nod /dev/mtdblock1 0600 0 0 b 31 1
nod /dev/mtdblock2 0600 0 0 b 31 2
nod /dev/mtdblock3 0600 0 0 b 31 3
nod /dev/mtdblock4 0600 0 0 b 31 4
nod /dev/mtdblock5 0600 0 0 b 31 5
dir /target 0755 0 0
dir /target/persist 0755 0 0
dir /target/nix 0755 0 0
file /init ${pkgs.preinit}/bin/preinit 0755 0 0
SPECIALS
'';
systemConfiguration =
pkgs.systemconfig config.filesystem.contents;
};
};
}

54
modules/jffs2.nix Normal file
View File

@ -0,0 +1,54 @@
{
config
, pkgs
, lib
, ...
}:
let
inherit (lib) mkIf mkOption types;
in
{
imports = [
./initramfs.nix
];
options.system.outputs = {
systemConfiguration = mkOption {
type = types.package;
description = ''
pkgs.systemconfig for the configured filesystem,
contains 'activate' and 'init' commands
'';
internal = true;
};
};
config = mkIf (config.rootfsType == "jffs2") {
kernel.config = {
JFFS2_FS = "y";
JFFS2_LZO = "y";
JFFS2_RTIME = "y";
JFFS2_COMPRESSION_OPTIONS = "y";
JFFS2_ZLIB = "y";
JFFS2_CMODE_SIZE = "y";
};
boot.initramfs.enable = true;
system.outputs = rec {
systemConfiguration =
pkgs.systemconfig config.filesystem.contents;
rootfs =
let
inherit (pkgs.pkgsBuildBuild) runCommand mtdutils;
endian = if pkgs.stdenv.isBigEndian
then "--big-endian" else "--little-endian";
in runCommand "make-jffs2" {
depsBuildBuild = [ mtdutils ];
} ''
mkdir -p $TMPDIR/empty/nix/store/ $TMPDIR/empty/secrets
cp ${systemConfiguration}/bin/activate $TMPDIR/empty/activate
ln -s ${pkgs.s6-init-bin}/bin/init $TMPDIR/empty/init
grafts=$(sed < ${systemConfiguration}/etc/nix-store-paths 's/^\(.*\)$/--graft \1:\1/g')
mkfs.jffs2 --compression-mode=size ${endian} -e ${config.hardware.flash.eraseBlockSize} --enable-compressor=lzo --pad --root $TMPDIR/empty --output $out $grafts --squash --faketime
'';
};
};
}

View File

@ -1,137 +0,0 @@
## Kernel-related options
## ======================
##
##
{ lib, pkgs, config, ...}:
let
inherit (lib) mkEnableOption mkOption types isDerivation hasAttr ;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs.liminix.networking) address interface;
inherit (pkgs.liminix.services) bundle;
inherit (pkgs) liminix;
type_service = pkgs.liminix.lib.types.service;
mergeConditionals = conf : conditions :
# for each key in conditions, if it is present in conf
# then merge the associated value into conf
lib.foldlAttrs
(acc: name: value:
if (conf ? ${name}) && (conf.${name} != "n")
then acc // value
else acc)
conf
conditions;
in {
options = {
kernel = {
src = mkOption { type = types.path; } ;
version = mkOption { type = types.str; default = "5.15.137";} ;
modular = mkOption {
type = types.bool;
default = true;
description = "support loadable kernel modules";
};
extraPatchPhase = mkOption {
default = "true";
type = types.lines;
};
config = mkOption {
description = ''
Kernel config options, as listed in Kconfig* files in the
kernel source tree. Do not include the leading "CONFIG_"
prefix when defining these. Most values are "y", "n" or "m",
but sometimes other strings are also used.
'';
type = types.attrsOf types.nonEmptyStr;
example = lib.literalExpression ''
{
BRIDGE = "y";
TMPFS = "y";
FW_LOADER_USER_HELPER = "n";
};
'';
};
conditionalConfig = mkOption {
description = ''
Kernel config options that should only be applied when
some other option is present.
'';
type = types.attrsOf (types.attrsOf types.nonEmptyStr);
default = {};
example = {
USB = {
USB_XHCI_MVEBU = "y";
USB_XHCI_HCD = "y";
};
};
};
makeTargets = mkOption {
type = types.listOf types.str;
};
};
};
config = {
system.outputs =
let
mergedConfig = mergeConditionals
config.kernel.config
config.kernel.conditionalConfig;
k = liminix.builders.kernel.override {
config = mergedConfig;
inherit (config.kernel) version src extraPatchPhase;
targets = config.kernel.makeTargets;
};
in {
kernel = k.vmlinux;
zimage = k.zImage;
};
kernel = rec {
modular = true; # disabling this is not yet supported
makeTargets = ["vmlinux"];
config = {
IKCONFIG = "y";
IKCONFIG_PROC = "y";
PROC_FS = "y";
KEXEC = "y";
MODULES = if modular then "y" else "n";
MODULE_SIG = if modular then "y" else "n";
DEBUG_FS = "y";
# basic networking protocols
NET = "y";
UNIX = "y";
INET = "y";
IPV6 = "y";
PACKET = "y"; # for ppp, tcpdump ...
SYSVIPC= "y";
NETDEVICES = "y"; # even PPP needs this
# disabling this option causes the kernel to use an "empty"
# initramfs instead: it has a /dev/console node and not much
# else. Note that pid 1 is started *before* the root
# filesystem is mounted and it expects /dev/console to be
# present already
BLK_DEV_INITRD = lib.mkDefault "n"; # overriden by initramfs module
# s6-linux-init mounts this on /dev
DEVTMPFS = "y";
# some or all of these may be fix for "tmpfs: Unknown parameter 'mode'" error
TMPFS = "y";
TMPFS_POSIX_ACL = "y";
TMPFS_XATTR = "y";
FW_LOADER = "y";
FW_LOADER_COMPRESS = "y";
# We don't have a user helper, so we get multiple 60s pauses
# at boot time unless we disable trying to call it.
# https://lkml.org/lkml/2013/8/5/175
FW_LOADER_USER_HELPER = "n";
};
};
};
}

View File

@ -7,7 +7,7 @@
let
inherit (lib) mkOption mkForce types concatStringsSep;
in {
imports = [ ../ramdisk.nix ];
imports = [ ./ramdisk.nix ];
options.system.outputs = {
kexecboot = mkOption {
type = types.package;

View File

@ -1,27 +0,0 @@
{ config, pkgs, lib, ...} :
let inherit (pkgs.liminix.services) oneshot longrun;
in {
config = {
services = rec {
mdevd = longrun {
name = "mdevd";
notification-fd = 3;
run = "${pkgs.mdevd}/bin/mdevd -D 3 -b 200000 -O4";
};
devout = longrun {
name = "devout";
notification-fd = 10;
run = "${pkgs.devout}/bin/devout /run/devout.sock 4";
};
coldplug = oneshot {
name ="coldplug";
# would love to know what mdevd-coldplug/udevadm trigger does
# that this doesn't
up = ''
for i in $(find /sys -name uevent); do ( echo change > $i ) ; done
'';
dependencies = [devout mdevd];
};
};
};
}

View File

@ -1,64 +0,0 @@
## Mount
##
## Mount filesystems
{ lib, pkgs, config, ...}:
let
inherit (lib) mkOption types;
inherit (pkgs) liminix;
mkBoolOption = description : mkOption {
type = types.bool;
inherit description;
default = true;
};
in {
options = {
system.service.mount = mkOption {
type = liminix.lib.types.serviceDefn;
};
};
imports = [ ../mdevd.nix ];
config.system.service.mount =
let svc = liminix.callService ./service.nix {
partlabel = mkOption {
type = types.str;
example = "my-usb-stick";
};
mountpoint = mkOption {
type = types.str;
example = "/mnt/media";
};
options = mkOption {
type = types.listOf types.str;
default = [];
example = ["noatime" "ro" "sync"];
};
fstype = mkOption {
type = types.str;
default = "auto";
example = "vfat";
};
};
in svc // {
build = args:
let args' = args // {
dependencies = (args.dependencies or []) ++ [
config.services.mdevd
config.services.devout
];
};
in svc.build args' ;
};
config.programs.busybox = {
applets = ["blkid" "findfs"];
options = {
FEATURE_BLKID_TYPE = "y";
FEATURE_MOUNT_FLAGS = "y";
FEATURE_MOUNT_LABEL = "y";
FEATURE_VOLUMEID_EXT = "y";
};
};
}

View File

@ -1,26 +0,0 @@
{
liminix
, uevent-watch
, lib
}:
{ partlabel, mountpoint, options, fstype }:
let
inherit (liminix.services) longrun oneshot;
device = "/dev/disk/by-partlabel/${partlabel}";
options_string =
if options == [] then "" else "-o ${lib.concatStringsSep "," options}";
mount_service = oneshot {
name = "mount.${lib.escapeURL mountpoint}";
timeout-up = 3600;
up = "mount -t ${fstype} ${options_string} ${device} ${mountpoint}";
down = "umount ${mountpoint}";
};
in longrun {
name = "watch-mount.${lib.strings.sanitizeDerivationName mountpoint}";
isTrigger = true;
buildInputs = [ mount_service ];
run = ''
${uevent-watch}/bin/uevent-watch -s ${mount_service.name} -n ${device} partname=${partlabel} devtype=partition
'';
}

View File

@ -1,29 +0,0 @@
{
liminix
, ifwait
, serviceFns
, lib
}:
{interface, family, address, prefixLength} :
let
inherit (liminix.services) oneshot;
# rather depending on the assumption that nobody will
# ever add two addresses which are the same but with different
# prefixes, or the same but different protocols
name = "${interface.name}.a.${address}";
up = ''
. ${serviceFns}
dev=$(output ${interface} ifname)
ip address add ${address}/${toString prefixLength} dev $dev
(in_outputs ${name}
echo ${address} > address
echo ${toString prefixLength} > prefix-length
echo ${family} > family
echo $dev > ifname
)
'';
in oneshot {
inherit name up;
down = "true"; # this has been broken for ~ ages
dependencies = [ interface ];
}

View File

@ -1,147 +0,0 @@
## Network
## =======
##
## Basic network services for creating hardware ethernet devices
## and adding addresses
{ lib, pkgs, config, ...}:
let
inherit (lib) mkOption types;
inherit (pkgs) liminix;
inherit (pkgs.liminix.services) bundle;
in {
options = {
system.service.network = {
link = mkOption {
description = "hardware network interface";
type = liminix.lib.types.serviceDefn;
};
address = mkOption {
description = "network interface address";
type = liminix.lib.types.serviceDefn;
};
route = mkOption {
type = liminix.lib.types.serviceDefn;
};
forward = mkOption {
type = liminix.lib.types.serviceDefn;
};
dhcp = {
client = mkOption {
# this needs to move to its own service as it has
# busybox config
description = "DHCP v4 client";
type = liminix.lib.types.serviceDefn;
};
};
};
};
config = {
hardware.networkInterfaces = {
lo =
let
net = config.system.service.network;
iface = net.link.build { ifname = "lo";};
in bundle {
name = "loopback";
contents = [
( net.address.build {
interface = iface;
family = "inet";
address ="127.0.0.1";
prefixLength = 8;
})
( net.address.build {
interface = iface;
family = "inet6";
address = "::1";
prefixLength = 128;
})
];
};
};
services.loopback = config.hardware.networkInterfaces.lo;
system.service.network = {
link = liminix.callService ./link.nix {
ifname = mkOption {
type = types.str;
example = "eth0";
description = ''
Device name as used by the kernel (as seen in "ip link"
or "ifconfig" output). If devpath is also specified, the
device will be renamed to the name provided.
'';
};
devpath = mkOption {
type = types.nullOr types.str;
default = null;
example = "/devices/platform/soc/soc:internal-regs/f1070000.ethernet";
description = ''
Path to the sysfs node of the device. If you provide this
and the ifname option, the device will be renamed to the
name given by ifname.
''; };
# other "ip link add" options could go here as well
mtu = mkOption {
type = types.nullOr types.int;
example = 1480;
};
};
address = liminix.callService ./address.nix {
interface = mkOption {
type = liminix.lib.types.service;
};
family = mkOption {
type = types.enum [ "inet" "inet6" ];
};
address = mkOption {
type = types.str;
};
prefixLength = mkOption {
type = types.ints.between 0 128;
};
};
route = liminix.callService ./route.nix {
interface = mkOption {
type = types.nullOr liminix.lib.types.interface;
default = null;
description = "Interface to route through. May be omitted if it can be inferred from \"via\"";
};
target = mkOption {
type = types.str;
description = "host or network to add route to";
};
via = mkOption {
type = types.str;
description = "address of next hop";
};
metric = mkOption {
type = types.int;
description = "route metric";
default = 100;
};
};
forward = liminix.callService ./forward.nix {
enableIPv4 = mkOption {
type = types.bool;
default = true;
};
enableIPv6 = mkOption {
type = types.bool;
default = true;
};
};
dhcp.client = liminix.callService ./dhcpc.nix {
interface = mkOption {
type = liminix.lib.types.service;
};
};
};
};
}

View File

@ -1,46 +0,0 @@
{
liminix
, writeAshScript
, serviceFns
, lib
} :
{ interface }:
let
inherit (liminix.services) longrun;
name = "${interface.name}.dhcpc";
script = writeAshScript "dhcp-notify" { } ''
. ${serviceFns}
exec 2>&1
action=$1
set_address() {
ip address replace $ip/$mask dev $interface
(in_outputs ${name}
for i in lease mask ip router siaddr dns serverid subnet opt53 interface ; do
printenv $i > $i
done)
}
case $action in
deconfig)
ip address flush $interface
ip link set up dev $interface
;;
bound)
# this doesn't actually replace, it adds a new address.
set_address
echo >/proc/self/fd/10
;;
renew)
set_address
;;
nak)
echo "received NAK on $interface"
;;
esac
'';
in longrun {
inherit name;
run = "/bin/udhcpc -f -i $(output ${interface} ifname) -x hostname:$(cat /proc/sys/kernel/hostname) -s ${script}";
notification-fd = 10;
dependencies = [ interface ];
}

View File

@ -1,21 +0,0 @@
{
liminix
, ifwait
, serviceFns
, lib
}:
{ enableIPv4, enableIPv6 }:
let
inherit (liminix.services) oneshot;
ip4 = "/proc/sys/net/ipv4/conf/all/forwarding";
ip6 = "/proc/sys/net/ipv6/conf/all/forwarding";
opt = lib.optionalString;
sysctls = b :
""
+ opt enableIPv4 "echo ${b} > ${ip4}\n"
+ opt enableIPv6 "echo ${b} > ${ip6}\n";
in oneshot {
name = "forwarding${opt enableIPv4 "4"}${opt enableIPv6 "6"}";
up = sysctls "1";
down = sysctls "0";
}

View File

@ -1,30 +0,0 @@
{
liminix
, ifwait
, serviceFns
, lib
}:
{
ifname
, devpath ? null
, mtu} :
# if devpath is supplied, we rename the interface at that
# path to have the specified name.
let
inherit (liminix.services) longrun oneshot;
inherit (lib) concatStringsSep;
name = "${ifname}.link";
rename = if devpath != null
then ''
oldname=$(cd /sys${devpath} && cd net/ && echo *)
ip link set ''${oldname} name ${ifname}
''
else "";
in oneshot {
inherit name;
up = ''
${rename}
${liminix.networking.ifup name ifname}
'';
down = "ip link set down dev ${ifname}";
}

View File

@ -1,20 +0,0 @@
{
liminix
, ifwait
, serviceFns
, lib
}:
{ target, via, interface ? null, metric }:
let
inherit (liminix.services) oneshot;
with_dev = if interface != null then "dev $(output ${interface} ifname)" else "";
in oneshot {
name = "route-${target}-${builtins.substring 0 12 (builtins.hashString "sha256" "${via}-${if interface!=null then interface.name else ""}")}";
up = ''
ip route add ${target} via ${via} metric ${toString metric} ${with_dev}
'';
down = ''
ip route del ${target} via ${via} ${with_dev}
'';
dependencies = [] ++ lib.optional (interface != null) interface;
}

View File

@ -22,7 +22,7 @@ let
++
(mapAttrsToList (name: opts: "peer ${name} ${concatStringsSep "" opts}")
p.peers)
++ lib.optional (p.user != null) "user ${p.user}"
++ [ "user #{p.user}" ]
++ (lib.optional (p.makestep != null) "makestep ${toString p.makestep.threshold} ${toString p.makestep.limit}")
++ (map (n: "allow ${n}") p.allow)
++ (lib.optional (p.bindaddress != null) "bindaddress ${p.bindaddress}")

View File

@ -7,136 +7,83 @@
let
inherit (lib) mkOption types concatStringsSep;
inherit (pkgs) liminix callPackage writeText;
o = config.system.outputs;
in
{
imports = [
./squashfs.nix
./outputs/vmroot.nix
./outputs/extlinux.nix
];
options = {
system.outputs = {
# the convention here is to mark an output as "internal" if
# it's not a complete system (kernel plus userland, or installer)
# but only part of one.
kernel = mkOption {
type = types.package;
internal = true;
description = ''
kernel
******
Kernel vmlinux file (usually ELF)
'';
};
zimage = mkOption {
type = types.package;
internal = true;
description = ''
zimage
******
Kernel in compressed self-extracting package
'';
};
dtb = mkOption {
type = types.package;
internal = true;
description = ''
dtb
***
Compiled device tree (FDT) for the target device
'';
};
uimage = mkOption {
type = types.package;
internal = true;
description = ''
uimage
******
Combined kernel and FDT in uImage (U-Boot compatible) format
'';
};
tplink-safeloader = mkOption {
type = types.package;
};
u-boot = mkOption {
vmroot = mkOption {
type = types.package;
description = ''
Directory containing separate kernel and rootfs image for
use with qemu (see mips-vm)
'';
};
manifest = mkOption {
type = types.package;
internal = true;
description = ''
Debugging aid. JSON rendition of config.filesystem, on
which can run "nix-store -q --tree" on it and find
out what's in the image, which is nice if it's unexpectedly huge
'';
};
rootdir = mkOption {
type = types.package;
internal = true;
description = ''
directory of files to package into root filesystem
'';
};
bootablerootdir = mkOption {
type = types.package;
internal = true;
description = ''
directory of files to package into root filesystem, including
a kernel and appropriate associated gubbins for the
selected bootloader
'';
};
rootfs = mkOption {
type = types.package;
internal = true;
description = ''
root filesystem (squashfs or jffs2) image
'';
internal = true;
};
};
};
config = {
system.outputs = rec {
dtb = liminix.builders.dtb {
# tftpd = pkgs.buildPackages.tufted;
kernel = liminix.builders.kernel.override {
inherit (config.kernel) config src extraPatchPhase;
};
dtb = (callPackage ../kernel/dtb.nix {}) {
inherit (config.boot) commandLine;
dts = config.hardware.dts.src;
includes = config.hardware.dts.includes ++ [
"${o.kernel.headers}/include"
"${kernel.headers}/include"
];
};
uimage = liminix.builders.uimage {
uimage = (callPackage ../kernel/uimage.nix {}) {
commandLine = concatStringsSep " " config.boot.commandLine;
inherit (config.boot) commandLineDtbNode;
inherit (config.hardware) loadAddress entryPoint alignment;
inherit (config.boot) imageFormat;
inherit (o) kernel dtb;
inherit (config.hardware) loadAddress entryPoint;
inherit kernel;
inherit dtb;
};
rootdir =
let
inherit (pkgs.pkgsBuildBuild) runCommand;
in runCommand "mktree" { } ''
mkdir -p $out/nix/store/ $out/secrets $out/boot
cp ${o.systemConfiguration}/bin/activate $out/activate
ln -s ${pkgs.s6-init-bin}/bin/init $out/init
mkdir -p $out/nix/store
for path in $(cat ${o.systemConfiguration}/etc/nix-store-paths) ; do
(cd $out && cp -a $path .$path)
done
'';
bootablerootdir =
let inherit (pkgs.pkgsBuildBuild) runCommand;
in runCommand "add-slash-boot" { } ''
cp -a ${o.rootdir} $out
${if config.boot.loader.extlinux.enable
then "(cd $out && chmod -R +w . && rmdir boot && cp -a ${o.extlinux} boot)"
else ""
}
'';
vmroot = pkgs.runCommand "qemu" {} ''
mkdir $out
cd $out
ln -s ${config.system.outputs.rootfs} rootfs
ln -s ${kernel} vmlinux
ln -s ${manifest} manifest
ln -s ${kernel.headers} build
'';
manifest = writeText "manifest.json" (builtins.toJSON config.filesystem.contents);
};
};

Some files were not shown because too many files have changed in this diff Show More