forked from dan/liminix
1
0
Fork 0

Compare commits

...

162 Commits
main ... main

Author SHA1 Message Date
Daniel Barlow 471c63b399 s6-rc do cleanup in "finish", don't append to "run" script
s6-supervise sends signals (e.g. SIGTERM) to the pid of the process
running "run", so how do we know if the ceanup commands are even
getting executed if the shell interpreter that is supposed to do that
got killed already?
2024-05-13 17:53:02 +01:00
Daniel Barlow 782feaeafa set default for firewall extraRules 2024-05-03 16:28:53 +01:00
Daniel Barlow ac54c89427 add busybox to bordervm for udhcpd 2024-05-01 23:09:23 +01:00
Daniel Barlow 5a3646cb29 add authorized keys to bordervm
You don't often need this because it has autologin, but sometimes
you want to do antics involving sshing through it to the wan port
of a test device.

Note that you probably wanted to start bordervm with funny qemu
options to even make that possible

 nix-shell --run "QEMU_NET_OPTS=hostfwd=tcp::10022-:22 run-border-vm"
2024-05-01 23:07:11 +01:00
Daniel Barlow e249f48cff add deps on {ins,rm}mod and kconfig for firewall module 2024-05-01 23:06:12 +01:00
Daniel Barlow 6661e42684 mt300a tftpboot needs appendDTB 2024-05-01 23:04:25 +01:00
Daniel Barlow b9ba9ef835 mt300a remove unneeded service dependencies 2024-05-01 23:03:55 +01:00
Daniel Barlow 8b69dcc209 pass entire config fragment to levitate, not just services
to make it useful we need to be able to set packages, passwords, ssh
keys etc
2024-04-29 20:07:01 +01:00
Daniel Barlow 9b3a3b9ff7 add levitate to arhcive
this is largely untested
2024-04-28 21:38:13 +01:00
Daniel Barlow 7d08497bcb arhcive remove coldplug fudge 2024-04-28 21:37:30 +01:00
Daniel Barlow 0e84adaa0e maybe don't need deps for gl-mt300a vlan devices?
will delete them next time I have that device open to test
2024-04-28 21:35:09 +01:00
Daniel Barlow 660ed5df8f vlan interface services depend on primary 2024-04-28 21:33:36 +01:00
Daniel Barlow 792a11c8c0 gl-mt300n-v2 use full path to swconfig in service stop 2024-04-28 21:32:42 +01:00
Daniel Barlow 7e4a05bbf8 separate kernel and base modules
this is needed for levitate
2024-04-28 12:44:27 +01:00
Daniel Barlow a4ba5c85e1 alphabetize list in all-modules 2024-04-28 12:42:47 +01:00
Daniel Barlow 723ef73d5a inout: test hotplug and coldplug 2024-04-27 22:41:30 +01:00
Daniel Barlow 3d4e782929 devout: run tests in postBuild
because checkPhase is not executed when cross-compiling, and this
package is always only cross-compiled
2024-04-27 21:07:25 +01:00
Daniel Barlow 1b6a05aec5 make uevent-watch use devout instead of direct netlink 2024-04-27 21:07:25 +01:00
Daniel Barlow 80628a3d90 move event matching tests to devout
in preparation for future uevent-watch not needing to do
event matching
2024-04-27 21:07:25 +01:00
Daniel Barlow bf0cafffed start devout alongside mdevd
ensure it starts before mdevd-coldplug so it can populate
its database
2024-04-26 20:52:12 +01:00
Daniel Barlow e49aba127c devout: improve socket error handling 2024-04-26 20:49:23 +01:00
Daniel Barlow 324465bc18 devout: write uevent KEY=value format to clients 2024-04-26 17:37:28 +01:00
Daniel Barlow b33249a050 devout: add readiness notification 2024-04-26 17:23:29 +01:00
Daniel Barlow b9c084415e devout: handle readiness on netlink socket but no event 2024-04-26 17:20:33 +01:00
Daniel Barlow cf9cadd212 devout: replay relevant events to new subscriber 2024-04-26 17:20:33 +01:00
Daniel Barlow a116fe084a devout: use socket constants from anoia.net.constants 2024-04-26 16:48:51 +01:00
Daniel Barlow 74cf3e0711 add anoia.net.constants for SOCK_{STREAM,DGRAM} etc
we use an ugly bit of C preprocessor to get the values from
header files, because certain constants are different on MIPS
than on other architectures
2024-04-26 16:43:09 +01:00
Daniel Barlow 9795f03da4 think 2024-04-26 16:41:31 +01:00
Daniel Barlow cdb23b147c convert anoia.fs to use lualinux 2024-04-25 21:14:37 +01:00
Daniel Barlow dbd1264352 convert anoia.fs to use lualinux instead of lfs 2024-04-24 20:44:32 +01:00
Daniel Barlow 834858d5bc think 2024-04-24 18:33:57 +01:00
Daniel Barlow 18335b95e3 devout: strip newlines from client terms
this is just to make testing with socat easier
2024-04-24 18:33:02 +01:00
Daniel Barlow 6bee2f67ac devout: add incoming netlink messages to database 2024-04-24 18:32:27 +01:00
Daniel Barlow b4ba3eea21 fix revents in unpack-pollfds 2024-04-24 18:31:26 +01:00
Daniel Barlow 16af3984c9 add lualinux to fennelrepl 2024-04-24 18:30:34 +01:00
Daniel Barlow ce7e395295 devout test: replace minisock with lualinux 2024-04-24 18:29:24 +01:00
Daniel Barlow 7e13e017eb add readline suport to fennelrepl 2024-04-24 18:28:39 +01:00
Daniel Barlow bbf2f53c0e cross-compile lualinux 2024-04-24 18:28:14 +01:00
Daniel Barlow 032d0f8aca add netlink socket
it's not hooked up to anything yet, but it proves we can
do this with lualinux
2024-04-23 23:34:25 +01:00
Daniel Barlow b8ac9e5279 convert devout from minisock to lualinux 2024-04-23 23:33:11 +01:00
Daniel Barlow ff2604ca5d think 2024-04-23 23:30:50 +01:00
Daniel Barlow 72789984ce add lualinux package 2024-04-23 22:41:38 +01:00
Daniel Barlow 90d9d0e811 update minisock to not scribble on lua strings 2024-04-23 20:19:33 +01:00
Daniel Barlow 97a8ae1c84 devout: add event loop and main `run` function 2024-04-23 20:15:02 +01:00
Daniel Barlow 52eb283a26 implement unsubscribe
and add ids to subscribe so that there's a unique identifier
to pass to unsubscribe
2024-04-23 20:12:46 +01:00
Daniel Barlow cbb1de804e switch to minisock fork witj poll() call
this is likely to be temporary as minisock is getting
replaced with lualinux
2024-04-23 20:09:41 +01:00
Daniel Barlow f9c03998b8 implement subscriptions with callback 2024-04-21 13:19:17 +01:00
Daniel Barlow 50de1b090f add the rest of the test list (all we've thought of) 2024-04-21 11:22:26 +01:00
Daniel Barlow 648382f64a report bodyless tests as PENDING 2024-04-21 11:19:42 +01:00
Daniel Barlow e9370358ae implement "remove" events 2024-04-21 11:19:06 +01:00
Daniel Barlow 762ce7b6b8 cut/paste devout implementation into a real module 2024-04-20 22:48:00 +01:00
Daniel Barlow b1c0560f4f implement fetch by path 2024-04-20 22:20:43 +01:00
Daniel Barlow e34135c41a improve failed test reporting 2024-04-20 21:46:37 +01:00
Daniel Barlow 712c9b266f implement find 2024-04-20 18:42:42 +01:00
Daniel Barlow 4df963996c devout: add device 2024-04-20 18:24:10 +01:00
Daniel Barlow 349bfecbb8 new package "devout", does nothing yet 2024-04-20 17:45:40 +01:00
Daniel Barlow 450d3820b2 clean up uevent-watch test using writeFennel and mainFunction
requires less cavorting with globals and stuff
2024-04-20 16:53:43 +01:00
Daniel Barlow 771585546d import expect= where previously it was copy-pasted 2024-04-20 15:09:50 +01:00
Daniel Barlow 73abf952d5 package minisock, a minimal Lua socket library 2024-04-20 15:09:17 +01:00
Daniel Barlow 8af4e9fd5b package anoia assert macros and point fennelrepl at them 2024-04-20 14:59:14 +01:00
Daniel Barlow 7e19d80130 anoia: add assert macro module
contains expect and expect=
2024-04-20 14:04:32 +01:00
Daniel Barlow 0f0688c802 think 2024-04-20 14:03:48 +01:00
Daniel Barlow b43f17f655 think 2024-04-20 12:23:04 +01:00
Daniel Barlow adf62d4483 arhcive: make it work when disk is attached before boot
This is a bit of a kludge (a lot of a kludge) but it will
get it running whilt I work on something better
2024-04-17 18:49:30 +01:00
Daniel Barlow 68eb1360f6 use appended dtb in gl-mt300n-v2 tftpboot
probably the A variant needs this as well
2024-04-17 18:48:19 +01:00
Daniel Barlow 19ad6cd278 watchdog: put s6 pkg on $PATH for s6-svstat 2024-04-17 13:01:10 +01:00
Daniel Barlow 00076c7b81 mount service: use uevent-watch 2024-04-17 12:59:13 +01:00
Daniel Barlow 721e7499f3 arhcive: use usb module instead of harcoded kconfig 2024-04-17 12:53:43 +01:00
Daniel Barlow fc723b9a35 think 2024-04-16 18:59:01 +01:00
Daniel Barlow a5f16dfa81 convert inout test to use uevent-watch 2024-04-15 22:15:27 +01:00
Daniel Barlow 41a4b1f7ef clean cruft from inout test script 2024-04-15 22:00:44 +01:00
Daniel Barlow 42a5699326 remove unneeded config from inout test 2024-04-15 21:19:18 +01:00
Daniel Barlow ea2b25168e add uevent-watch, which toggles services based on uevent msgs 2024-04-15 21:15:07 +01:00
Daniel Barlow 5564cf0554 add nellie.close 2024-04-14 22:45:29 +01:00
Daniel Barlow f3a13630d3 add multicast groups param to nellie.open 2024-04-14 22:45:29 +01:00
Daniel Barlow f233acf9ff netlink uevent hello world 2024-04-14 22:45:29 +01:00
Daniel Barlow b6a054c588 add mdevd as module
following the upstream example, it republishes uevent messages
using multicast group 4 instead of group 2 as used by udev.
2024-04-14 21:59:23 +01:00
Daniel Barlow b231664a06 anoia: add basename, dirname 2024-04-11 23:11:20 +01:00
Daniel Barlow f4bf3029fa anoia: alphabetize exports 2024-04-11 23:11:13 +01:00
Daniel Barlow 05f2c9a2f7 add lua in nix-shell environment 2024-04-11 23:11:06 +01:00
Daniel Barlow 5df5c822ea convert mount service to trigger
Good: this means it's not hanging holding the s6 dataase lock.

Bad: it's the ugliest implementation and doesn't deserve to be preserved

(tbf the ugliness is not new)
2024-04-03 23:17:36 +01:00
Daniel Barlow 4795dd05b7 unconditionally restart trigger services on liminix-rebuild
We call s6-rc -u -p default to restart/start the base services
on a rebuild, otherwise services that are only in the new
configuration won't come up. However, this stops any service
started by a trigger. So, workaround is to restart the trigger
service and expect it to restart the services it manages if they're
needed
2024-04-03 23:07:56 +01:00
Daniel Barlow a192f08881 remove missing module 2024-03-29 17:34:10 +00:00
Daniel Barlow a873dc6608 Merge commit 'efcfdcc' 2024-03-28 23:47:04 +00:00
Daniel Barlow 2fb4756a7f add soft restart option to liminix-rebuild
instead of doing a full reboot, it runs activate / and uses
s6-rc-update to install the new service database
2024-03-28 23:45:10 +00:00
Daniel Barlow 04f5174425 fix vanilla-configuration defaultroute 2024-03-28 22:13:21 +00:00
Daniel Barlow dca2e4def1 fix params to s6-rc-init
flags must precede scandir otherwise they're ignored
2024-03-28 21:56:28 +00:00
Daniel Barlow b60126775a improve liminix-rebuild test
* make it executable
* improve robustness
* do't hardcode services.default (why did it do this?)
2024-03-28 21:37:47 +00:00
Daniel Barlow 76f11bcc93 liminix-rebuild: remove -f flag from reboot call
now we have timeouts in service definitions, shouldn't need this
any more
2024-03-28 21:37:47 +00:00
Daniel Barlow efcfdcc21d think 2024-03-28 20:59:39 +00:00
Daniel Barlow 77f1a78331 ifwait block if s6-rc lock is held
otherwise it doesn't trigger the service if something else is
slow to start
2024-03-28 20:59:39 +00:00
Daniel Barlow 28a5dec7dd implement ifwait trigger service and use in bridge
should we convert all ifwait uses to this trigger too? seems
reasonable
2024-03-28 20:59:39 +00:00
Daniel Barlow fad0a47b75 add config.system.callService
this is like pkgs.callService except that it passes
config.system.service as a param so that the service
being defined can invoke other services

if this proves to be a good idea, all uses of
pkgs.callService should be changed to use it instead
2024-03-28 20:59:39 +00:00
Daniel Barlow af52aafc84 deep thoughts 2024-03-28 20:59:39 +00:00
Daniel Barlow 34442b6069 failing test for ifwait 2024-03-28 20:59:39 +00:00
Daniel Barlow b8a46fc05e allow buildInputs param to s6 service
this is in preparation for trigger services that need to
close over the triggered service without adding it to
s6-rc dependencies
2024-03-28 20:58:53 +00:00
Daniel Barlow 8ac2c6cec1 support timeouts (default 30s) for starting s6-rc services 2024-03-28 20:58:47 +00:00
Daniel Barlow 8879b2d1ba fix rt2x00 wifi 2024-03-28 20:58:39 +00:00
Daniel Barlow 83e346d5a0 add deviceName param 2024-03-22 21:55:44 +00:00
Daniel Barlow 156b1fe64a deep thoughts 2024-03-22 21:54:38 +00:00
Daniel Barlow 1a314e55b7 firewall module: provide default rules and merge extraRules
a firewall with no configuration will get a relatively sane ruleset. a
firewall with `extraRules` will get them deep merged into the default
rules.  Specifying `rules` will override the defaults
2024-03-21 12:00:34 +00:00
Daniel Barlow 9263b21faa create gateway profile by extracting from rotuer example 2024-03-21 10:04:42 +00:00
Daniel Barlow 0a820a702a extneder: delete nftables kernel config
don't need nftables on a bridge. (do we? hope not)
2024-03-20 19:05:31 +00:00
Daniel Barlow 4ea518e296 expose modulesPath to ease out-of-tree configuration.nix 2024-03-20 18:58:44 +00:00
Daniel Barlow 98318b450d deep thoughts 2024-03-16 20:16:49 +00:00
Daniel Barlow e4ac7f19dc fix ifwait deps 2024-03-16 20:16:49 +00:00
Daniel Barlow 9c22744850 deep thoughts 2024-03-16 20:16:49 +00:00
Daniel Barlow c697be8c28 temporary fix for cmake cross-compilation 2024-03-16 20:16:49 +00:00
dan 202a37221a Merge pull request 'tftpboot: use commandLineDtbNode' (#11) from flokli/liminix:tftpboot-honor-commandLineDtbNode into main
Reviewed-on: dan/liminix#11
2024-03-16 18:18:18 +00:00
Florian Klink 436eb03a7b tftpboot: use commandLineDtbNode
config.boot.commandLineDtbNode can be set from `bootargs` to
`bootargs-override` (used for boards where the u-boot on the board does
set `bootargs` on its own).

In that case, the code updating the cmdline for tftpboot purposes also
needs to update this node, not the `bootargs` node.

Otherwise the kernel won't find the phram device, as it never heard
about it, as it didn't get the necessary cmdline options.
2024-03-16 20:06:38 +02:00
Daniel Barlow e5963ae3f7 deep thoughts 2024-03-06 23:19:47 +00:00
Daniel Barlow f164f19d95 service starts and stops 2024-03-06 23:19:47 +00:00
Daniel Barlow dd4ab41f6a rename run-event 2024-03-06 23:19:47 +00:00
Daniel Barlow 5d5dff6729 WIP add failing test that service starts 2024-03-06 23:19:47 +00:00
Daniel Barlow 570d29c368 pass command line params to run instead of reffing global 2024-03-06 23:19:47 +00:00
Daniel Barlow 725af00dc9 improve test for dummy0 up
if we run off the end of the events fixture, it didn't work
2024-03-06 23:19:47 +00:00
Daniel Barlow e1b932ec27 remove hardcoded filename in test event generator 2024-03-06 23:19:47 +00:00
Daniel Barlow 7173b6fb1c don't call os.exit 2024-03-06 23:19:47 +00:00
Daniel Barlow ed9548f21d pass event producer fn as param 2024-03-06 23:19:47 +00:00
Daniel Barlow 0787807a7f ifwait: don't run on load if in test harness 2024-03-06 23:19:47 +00:00
Daniel Barlow 38ed91f641 simplify assertion 2024-03-06 23:19:47 +00:00
Daniel Barlow ffe9603c39 remove file-scoped parameters var 2024-03-06 23:19:47 +00:00
Daniel Barlow cbd3dfefc5 ifwait fixture/test harness 2024-03-06 23:19:47 +00:00
Daniel Barlow 018c1868b5 ifwait: use anoia.assoc 2024-03-06 23:19:47 +00:00
Daniel Barlow 5184ff63f7 add anoia.nl, a convenience wrapper on netlink 2024-03-06 23:19:47 +00:00
Daniel Barlow 35909c9a23 add netlink to fennelrepl 2024-03-06 23:19:47 +00:00
Daniel Barlow 4383462199 deep thoughts 2024-03-06 23:19:47 +00:00
Daniel Barlow 9730cdd63b add assoc to anoia 2024-03-06 23:19:47 +00:00
dan 095853214b Merge pull request 'Fix kernel build on belkin' (#10) from sinavir/liminix:fix_kernel_build_on_belkin into main
Reviewed-on: dan/liminix#10
2024-03-06 18:21:13 +00:00
Daniel Barlow 9d6e50cbbc extract extneder example to a "profile"
this is a bit of an experiment to reduce the copy-paste in
examples by turning them into "application" modules.

planning to follow up with another module for "wifi router"
2024-02-27 23:13:12 +00:00
Daniel Barlow 94dbc56595 fix doc 2024-02-27 20:08:30 +00:00
Daniel Barlow 2cd7f932eb alignment may be null 2024-02-27 19:47:46 +00:00
sinavir 27c7735f02 belkin-RT3200: fix kernel options 2024-02-22 21:57:40 +01:00
sinavir 29c9de248d fix import of openwrt sources 2024-02-22 21:57:33 +01:00
Daniel Barlow 3ca0d87c27 ci.nix: alphabetise systems 2024-02-21 19:49:14 +00:00
Daniel Barlow 8f30db58ae New port to Zyxel NWA50AX: update NEWS and ci.nix 2024-02-21 19:32:50 +00:00
Daniel Barlow f9ab0590a6 Merge remote-tracking branch 'raito/nwa50ax' 2024-02-21 19:27:23 +00:00
Daniel Barlow 84fa8d65f4 fennel: system: verbose log of command that was run 2024-02-21 19:27:14 +00:00
Daniel Barlow 9b0149ecb7 deep thoughts 2024-02-21 19:26:33 +00:00
Raito Bezarius baf3cf7413 devices/zyxel-nwa50ax: fix dual image mgmt after DTB expansion
Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 03:13:35 +01:00
Raito Bezarius c5145b5fc9 devices/zyxel-nwa50ax: make `zyxel-bootconfig` executable
Otherwise, it doesn't work well…

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 03:13:21 +01:00
Raito Bezarius 628f4dfdbe devices/zyxel-nwa50ax: developer todo
Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 03:13:21 +01:00
Raito Bezarius da59e2a349 devices/zyxel-nwa50ax: complete documentation
It covers everything I know more or less.

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 02:57:34 +01:00
Raito Bezarius c0a9571a13 devices/zyxel-nwa50ax: upgrade MT7915 firmware from OpenWRT repository
Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 02:57:34 +01:00
Raito Bezarius d6ffdd7be6 devices/zyxel-nwa50ax: expose primary and secondary images
To support A/B a bit better.

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 02:57:34 +01:00
Raito Bezarius 985f982435 examples/nwa50ax-ap: support bridge between lan and ethernet
Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 02:48:50 +01:00
Raito Bezarius a893c0dc4c devices/zyxel-nwa50ax: use our own more advanced DTB
OpenWRT had a DTB for the NWA50AX LEDs that I didn't pick up.

Anyway, we need to include our own special DTB for the NWA platform in general
to support A/B operations, because OpenWRT original one just mark everything else read-only.

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 02:48:50 +01:00
Raito Bezarius 3ec29dc1b9 examples/nwa50ax-ap: ensure `mtdutils` is available for further flashing
Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 02:48:50 +01:00
Raito Bezarius 0e81953b67 devices/zyxel-nwa50ax: cleanup of `flash` attribute and `rootDevice`
Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 02:48:50 +01:00
Raito Bezarius 3c70a0d037 devices/zyxel-nwa50ax: ensure bridge is always available
Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 02:48:50 +01:00
Raito Bezarius 422f3edab1 modules/zyxel-dual-image: init
This adds a simple boot blessing module, to be used, with the Zyxel NWA50AX.

There's a lot of elephant in the rooms: how do you upgrade kernel, etc.

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 02:48:50 +01:00
Raito Bezarius c14b2f6356 modules/busybox: add `dhcprelay`
This enables to run a DHCP relay from multiple interfaces.

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 02:48:50 +01:00
Raito Bezarius cdafff2095 examples/nwa50ax-ap: init
This is a quite comprehensive example using maximally the hardware
available to reach nice performance.

In the future, I will even add RADIUS examples.

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-19 02:48:50 +01:00
Raito Bezarius 13f1bb9f52 devices/zyxel-nwa50ax: init 2024-02-19 02:48:48 +01:00
Raito Bezarius 019fef6929 zyxel-bootconfig: init at no version
This tool is useful for manipulating the A/B boot status of the image.
2024-02-18 20:30:41 +01:00
Raito Bezarius 63007859c2 modules/outputs/zyxel-nwa-fit: init
Zyxel "firmware" format is just… a FIT with some metadata on the models.

This FIT is like this:

--------------------------
    uImage FIT header
--------------------------
    Linux kernel
--------------------------
    FDT DTB
--------------------------
    Padding so that
    this makes
    8192kb [1]
--------------------------
    UBI volume
    as a root filesystem
--------------------------

We just reproduce this in a very brutal and naive way.
In the future, this seems worth to generalize and modularize this idea
so that zyxel-nwa-fit is just an instance of a more general output.

[1]: https://git.openwrt.org/?p=openwrt/openwrt.git;a=blob;f=target/linux/ramips/image/mt7621.mk;h=ab1b829ba0086cb9fc9ca8cbbf3cbc14735034d6;hb=refs/heads/main#l3097

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-18 20:30:41 +01:00
Raito Bezarius e9ab8d7183 modules/outputs/ubivolume: introduce ubinization
It creates an UBI image based on an UBI volume configuration.

For now, it creates only an empty rootfs.
2024-02-18 20:30:41 +01:00
Raito Bezarius 3dc58de0eb modules/outputs: expose `commandLineDtbNode` option
We allow `bootargs` and `bootargs-override` for now only.

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-18 20:30:41 +01:00
Raito Bezarius dde8386f75 builders/uimage: support aligning the FIT
This is necessary when writing to a MTD partition with a certain erasesize.
2024-02-18 20:30:41 +01:00
Raito Bezarius c59364d623 modules/outputs/ubifs: expose `rootubifs` rather than `rootfs`
I believe there should be another module exposing `rootubifs` as `rootfs`
or let any other module just subsume that component like `zyxel-nwa-fit` output.

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-18 20:30:41 +01:00
Raito Bezarius b76c5b4abe modules/ubifs: revamp to offer directly access to the UBIfs partition
Adds the LEB and PEB option and let the user remove the boot image in case
where U-Boot does not support UBI boot.
2024-02-18 20:30:41 +01:00
Raito Bezarius 0a8343be66 pkgs/kernel/uimage: introduce `commandLineDtbNode`
Certain devices like the Zyxel NWA50AX will pass information on the command-line
to explain what is the current image (`bootImage=1` vs. `bootImage=0`).

Unfortunately, if we set the `chosen/bootargs` node, this will be overridden forcibly
by U-Boot.

To avoid this problem, it's easier to simply just use another DTB node like `bootargs-override` which
is what OpenWRT does [1].

[1]: https://git.openwrt.org/?p=openwrt/openwrt.git;a=blob;f=target/linux/ramips/patches-5.15/314-MIPS-add-bootargs-override-property.patch;h=e7dca7af886e8c0b69ba2b23f5855ddfeeb0d4a1;hb=refs/heads/main

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-02-18 20:30:41 +01:00
102 changed files with 4175 additions and 480 deletions

25
NEWS
View File

@ -34,7 +34,7 @@ Upstream changes that have led to incompatible Liminix changes are:
2024-01-30
New port! Thanks to Arnout Engelen <arnout@bzzt.net>, Liminix
now runs on the TP-Link Archer AX23
now runs on the TP-Link Archer AX23.
2024-02-12
@ -80,3 +80,26 @@ 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

@ -4111,3 +4111,633 @@ that is no longer in new-addresses
If the upstream has changed length, "ip addr change" is ignored,
so it needs to be in deleted as well as added/changed
Fri Feb 16 19:37:08 GMT 2024
[ 3.839775] cfg80211: module verification failed: signature and/or required key missing - tainting kernel
[ 4.156952] ath10k_pci 0000:00:00.0: enabling device (0000 -> 0002)
[ 4.165756] ath10k_pci 0000:00:00.0: pci irq legacy oper_irq_mode 1 irq_mode 0 reset_mode 0
[ 4.399285] ath10k_pci 0000:00:00.0: qca9887 hw1.0 target 0x4100016d chip_id 0x004000ff sub 0000:0000
[ 4.408906] ath10k_pci 0000:00:00.0: kconfig debug 1 debugfs 0 tracing 0 dfs 0 testmode 0
[ 4.420096] ath10k_pci 0000:00:00.0: firmware ver 10.2.4-1.0-00047 api 5 features no-p2p,ignore-otp,ski
p-clock-init,mfp,allows-mesh-bcast crc32 62f7565f
[ 4.467443] ath10k_pci 0000:00:00.0: board_file api 1 bmi_id N/A crc32 546cca0d
[ 5.472096] ath10k_pci 0000:00:00.0: htt-ver 2.1 wmi-op 5 htt-op 2 cal file max-sta 128 raw 0 hwcrypto
[ 5.585796] ath: EEPROM regdomain: 0x0
[ 5.589712] ath: EEPROM indicates default country code should be used
[ 5.596364] ath: doing EEPROM country->regdmn map search
[ 5.601875] ath: country maps to regdmn code: 0x3a
[ 5.606831] ath: Country alpha2 being used: US
[ 5.611425] ath: Regpair used: 0x3a
[ 6.742365] ath10k_pci 0000:00:00.0: pdev param 0 not supported by firmware
[ 6.903389] random: hostapd: uninitialized urandom read (1027 bytes read)
[ 8.169901] ath10k_pci 0000:00:00.0: pdev param 0 not supported by firmware
[ 14.450193] ath10k_pci 0000:00:00.0: pdev param 0 not supported by firmware
[ 15.518682] random: hostapd: uninitialized urandom read (1027 bytes read)
[ 16.762697] ath10k_pci 0000:00:00.0: pdev param 0 not supported by firmware
[ 23.030622] ath10k_pci 0000:00:00.0: pdev param 0 not supported by firmware
[
Tue Feb 27 23:16:27 GMT 2024
We made it a full week with rotuer running internet chez nous and no
need for an intervention, so I am happy to call it "production". There are
still things that need fixing but they're mostly within scope for
a services refresh
I have embarked on "profiles" by creating a wap.nix
I think we could have a service module for resolvconf
It would be good to build a wap.nix example for the belkin and we
could start looking at ubifs
I've lost a chunk of notes about using events to drive desired service
state. There is probably only going to be one udev listener, so
what if we have udev as a config key thusly
udev.rules = [
{
match = {
SUBSYSTEM = "rpmsg";
ATTR.name = "DATA5_CNTL";
};
service = longrun {
name = "lte-modem";
run = "blah blah blah";
};
}
# this one would be provided by the bridge module instead of
# adding bridge member services to the default target
{
match = {
SUBSYSTEM="net";
ID_PATH="pci-0000:04:00.0";
ATTR.operstate = "up";
};
service = oneshot {
up = "ip link set dev $dev master $(output ${primary} ifname)";
down = "ip link set dev $(output ${member} ifname) nomaster";
}
}
]
This works for udev/sysfs, but we want a similar architecture(sic) for
user-generated target state so we could have services that run on e.g.
"is the ppp0 service healthy" or not. Probably there isn't a top-level
config key for each service though
services.wan = svc.ppoe.build { .... };
services.lte = watcher.build {
watching = services.wan;
match = {
# an expression matching the outputs of the service
# to be watched
health = "failing";
};
service = oneshot {
run = "start_lte_blah";
};
}
thing is, we could use this syntax also for sysfs watches, but not vice versa
... but it's not quite the same because here we're doing static matches
on contents of files, whereas the udev one is a query expression on the
sysfs database. we might need that flexibiity to implement "mount the
backup drive no matter _which_ damn sda_n_ device it appears as". I don't
know if there's the same need for service outputs - postulate the
existence of a collection of services which are all similar enough that
some other service can watch them all and do $something when one of
the changes state. Or a single service with very complicated outputs.
For example, something could watch the snmp database and update service
status depending on what it finds. Or something something mqtt...
we find that the "match" needs to be interpreted differently according
to the thing being watched. perhaps the service being watched needs to
provide a "watch me" interface somehow which accepts match criteria and
outputs a true/false. Something else then needs to
services.addmember = services.udev.watch {
match = {
SUBSYSTEM = "net";
ID_PATH = "pci-0000:04:00.0";
ATTR.operstate = "up";
};
service = oneshot {
up = "ip link set dev $dev master $(output ${primary} ifname)";
down = "ip link set dev $(output ${member} ifname) nomaster";
};
}
Sat Mar 2 15:37:29 GMT 2024
Simply put, what I think it boils down to is that we want a service
which acts as an actuator or control switch for another service,
and will start/stop that controlled service according to some
criteria.
services.addmember = svc.network.ifwatch.build {
interface = config.hardware.networkInterfaces.lan1;
# this should be part of the definition not the params
service = oneshot {
name = "member-${bridge}-${interface}";
up = "ip link set dev $dev master $(output ${primary} ifname)";
down = "ip link set dev $(output ${member} ifname) nomaster";
};
}
we could start by writing this. we need to adapt ifwait
Sun Mar 3 17:09:21 GMT 2024
this is annoyingly hard to test. the tests we'd like to write are
1) when it gets events that don't match the requirement, nothing happens
2) when it gets an event that should start the service, the
service starts
3) when stop should stop
4) when start and already started, nothing happens
5) when stop and already stopped, nothing happens
what do we do if service fails to start? s6-rc will eventually reset it
to "down", I think: do we need to take action?
Mon Mar 4 20:46:55 GMT 2024
# relevant but not correct for this model: https://www.forked.net/forums/viewtopic.php?f=13&t=3490
# power on port 5
snmpset -v 1 -c private 192.168.5.14 .1.3.6.1.4.1.318.1.1.4.4.2.1.3.5 integer 1
# power off port 5
snmpset -v 1 -c private 192.168.5.14 .1.3.6.1.4.1.318.1.1.4.4.2.1.3.5 integer 2
# toggle off/on port 5
snmpset -v 1 -c private 192.168.5.14 .1.3.6.1.4.1.318.1.1.4.4.2.1.3.5 integer 3
Wed Mar 6 18:24:29 GMT 2024
What happens when we attempt to start the service but it fails? We
assume the start was successful so we won't try and restart it again
next time we get an event that should cause it to start.
Thu Mar 7 11:48:26 GMT 2024
what next?
- fennel script needs to know where s6-rc is
- some nix syntax
- update bridge module members.nix to use the new thing
I can't find a ci derivation that uses the bridge.
Mon Mar 11 20:31:45 GMT 2024
Create a qemu config where wan and lan devices are bridged into a
single bridge
start qemu paused
Use qemu monitor commands to no-carrier the network devices
set_link virtio-net-pci.1 off
set_link virtio-net-pci.0 off
Boot the system
See if both devices are bridge members
See if reboot is possible
Use qemu monitor commands to enable the network devices
set_link virtio-net-pci.1 on
set_link virtio-net-pci.0 on
See if both devices are bridge members
disable again,check if back to starting position
Wed Mar 13 00:00:16 GMT 2024
aside: "trigger" is the least bad word I've thought of so far for
these services that stop/start other services
telent: yeah, in general 'ps afuxww' (or s6-ps -H :)) is the way to solve this, look for hung s6-rc processes and in particular their s6-svlisten1 children, where the command line will show what service is still waiting for readiness
Wed Mar 20 19:34:36 GMT 2024
Because I forgot hoe to rebuild rotuer, I tihnk it is time to improve
support for out-of-tree configurations. So I've made
modules/profiles/gateway.nix and now I can copy rotuer.nix to
telent-nixos-config.
Probably I should make nix-build work on the top-level derivation
and install liminix-rebuild as a binary?
would be good if an out-of-tree config could specify the device
it was targeting?
Fri Mar 22 20:49:54 GMT 2024
Ideally liminix-rebuild could accept a configuration file that
specifies a liminix-config file, a target hostname (maybe plus ssh
port, credentials etc) and the device name. Not going to work on that
just now but it does mean we can punt on specifying the device inside the
liminix-config which is unreasonably circular.
Maybe we'll just chuck a makefile in telent-nixos-config
Fri Mar 22 22:14:32 GMT 2024
For the service failover milestone we said
a. A configuration demonstrating a service which is restarted when it crashes
b. A failover config where service B runs iff service A is unavailable
c. A config showing different pppd behaviour when interface is flakey (retry) vs ppp password is wrong (report error, wait for resolution)
Sun Mar 24 23:41:27 GMT 2024
TODO
1) make liminix-rebuild bounce only affected services instead of
full reboot (what does it do about triggered services?)
2) sniproxy
3) see if arhcive still works. usb disk hotplug would be a good candidate for
switching to triggers
Mon Mar 25 19:35:47 GMT 2024
to make the liminix-rebuild thing restart only affected services, it needs to
know when the new service is not like the old one. By default it does not
restart a service with a changed up/down/run script unless the name has
also changed, so we need to figure out how to generate a "conversion"
file with the services that are different
pkgs/s6-rc-database/default.nix creates $out/compiled, we could add
$out/hashes to this
the other thing making this fun is that we will need to run `activate`
(which is usually done in preinit) otherwise the new configuration's
fhs directories won't exist.
so the plan woyuld be
in liminix-rebuild, when reboot was not chosen,
- run activate
- compare /run/s6-rc/compiled/hashes (old services) with
/etc/s6-rc/compiled/hashes (new services)
- whenever both files have the same column 1 and different
column 2, add that name to restart list
(need to turn restarts.fnl into a lua script)
s6-rc-update /etc/s6-rc/compiled/hashes restarts
Tue Mar 26 23:18:53 GMT 2024
activate overwrites /etc/s6-rc/compiled, which is a problem because
s6-rc-update expects to find the old compiled database here so that
it can know what to update
Maybe config.filesystem should specify /etc/s6-rc/compiled.new
and something in early boot could symlink /etc/s6-rc/compiled to it
Sat Mar 30 18:41:14 GMT 2024
soft restart doesn't restart services that are invoked by trigger,
because it has to do -p -u default so that it prunes services that
were in the old config but not the new one. Ideally we need somehow
to notify the trigger that it should respawn its service. Maybe
we could add triggers to the force restart list, if there's a way
to detect which they are? don't want to do it by adding files in
the service state directory if there may be oneshot triggers. Can
there be oneshot triggers?
The hashes file is built when we build the service database, so we
could easily(?) add something in there to mark services that
need poking whenever there's a restart. It's not perfect because the
triggered services will be bounced unnecessarily, but remember that
the alternative is a reboot ...
Mon Apr 1 00:18:50 BST 2024
i) I don't know if digressing into remote log shipping is a tangent or
an important part of making services work well.
ii) Should there be a single "machine state" value for all of the
trigger services to reference, or is it better that each trigger
service has its own private state, or (third option) one state
per "state source"? We previously handwaved that a state source
is a service
services.addmember = services.udev.watch {
match = {
SUBSYSTEM = "net";
ID_PATH = "pci-0000:04:00.0";
ATTR.operstate = "up";
};
service = oneshot {
up = "ip link set dev $dev master $(output ${primary} ifname)";
down = "ip link set dev $(output ${member} ifname) nomaster";
};
}
Tue Apr 2 19:55:25 BST 2024
We could do a test script for udev usb disk mounting, which uses the
qemu monitor to add/remove a disk.
./result/run.sh --flag -device --flag usb-ehci,id=xhci --flag -drive --flag if=none,id=usbstick,format=raw,file=./stick.img
(qemu) device_add usb-storage,bus=xhci.0,drive=usbstick
Fri Apr 5 17:11:46 BST 2024
1) write a fennel thing that reads from the udev rebroadcast socket
2) and can check sysfs for state
3) set up mdevd in liminix
Sat Apr 6 13:23:02 BST 2024
I wonder if we could replace preinit with an execline script? One for
the TODO stack
Sun Apr 7 14:03:29 BST 2024
1) we want to know what messages are sent from mdevd under various circumstances
- actually, right now the only relevant circumstances are updown and inout
2) we might get a wider variety of messages from real hardware?
3) if we log the raw messages, pref. with timestamps, then we can
write tests for the parsing
therefore: write a program that opens the netlink socket and logs
all data received
----
what's the minimum we need here? we need the inout test to open a
uevent socket and use uevents to update some state that says whether the
backup drive is plugged in
rather awkwardly, uevents don't have filesystem labels. so we also need
to run blkid to find the label of each partition, and ideally we do this
while the partition is present, not each time we get an event for it.
We have DEVNAME, DEVTYPE, SUBSYSTEM to indicate that a filesystem of interest
may be present, we should use that as a trigger to scan any known
add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda/sda1
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda/sda1
SUBSYSTEM=block
MAJOR=8
MINOR=1
DEVNAME=sda1
DEVTYPE=partition
DISKSEQ=2
PARTN=1
SEQNUM=1528
Some disks on loaclhost and noetbook have PARTNAME field - I assume
this is because they're GPT disks. Would it actually be better to use this
field than grovelling for filesystem label?
Tue Apr 9 21:07:50 BST 2024
Having waited for the appropriately labelled disk to appear, we then
also have to communicate its path to the service that mounts it
- create a symlink
- or use an instanced service
Creating a symlink will be fine if we can pass the symlink name as
a param to fswait
Wed Apr 10 20:53:48 BST 2024
We think that fswait will evolve into a more general
waiting-for-uevents tool. Maybe we could provide the matchers on the
command line:
waituevent -l /dev/volumes/backup-disk -s mount-srv devtype=partition partname=backup-disk
Thu Apr 11 23:09:43 BST 2024
lcommit d3a2e3a4cb80b631df2ab79d463c2c4d1adef37b
commit 4a58cf9335116ce673fcf08f70f3bca921a4c9ad
commit afca6d4b63dd39062f02827b3c29e16904770216
Sun Apr 14 19:50:27 BST 2024
how to get this on to main:
- make uevent-watcher package (it's fswait renamed)
- make mount service use it
- module for mdevd
- add nellie (generalise for other netlink uses w/params pid/family/groups)
Mon Apr 15 19:59:43 BST 2024
plan:
introduce uevent-watcher command, update test to use it
make mount service use it
Tue Apr 16 18:59:25 BST 2024
Another idea for maybe-not-now: tftp local/peer addresses could be
provided as top-level params (e.g. to nix-build).
Wed Apr 17 18:57:49 BST 2024
I hatched a plan (and forgot to save this file) to build a service
that subscribes to uevents and retains state so that other services
can know about things that happened before they started. I'm wondering
if it's really needed though, because there could be one process to
read the socket and start/stop *all* the udev triggered services. Not
sure how we'd describe this in nix though: how do all the other
services
How we would do a uevent database service (sysfsq):
for each event e from socket
if e.action in (add, change)
path[e.path] = e.attribues
if e.action == 'remove'
path.remove e.path
(update-indices e)
(fn update-indices [event]
for each k in (keys event)
index.k.v += e)
we also want to not maintain indexes when there are so many values in
the index entry to make searching it worthless.
to retrieve, look at each criterion that has an index and choose the
index with fewest elements in the value. scan that index for the other
criteria
there are 813 uevent files in sysfs on arhcive, is this all overkill?
maybe we could simplify using a hardcoded stopword list - e.g. don't
have indices for MAJOR, MINOR
what are we going to use for querying? can't be netlink because that's
a shared medium (broadcast/multicast). unix dgram socket? alternative
would be to somehow use the filesystem as a database
Wed Apr 17 22:00:29 BST 2024
tests. assuming the sysfs setup from all-events.txt, we can write tests lik
- there is a path for $foo
- the attributes are x, y, z
- when I add a device with $attributes, I can recall it
- by path
- by attribute value
- when I remove it again, I cannot access it by path or attributes
- when I add a device with $attributes major minor foo bar baz
it is added to indices for foo bar baz but not major minor
- when I remove it, it can no longer be found by looking in any index
- when I query with multiple attributes, the search is performed
using the most specific attribute (= the attribute whose
value at this key has fewest elements)
I am still looking for ways to avoid doing this, but it is potentially
the first of several "database" services that triggers could want to
use so maybe it's an emerging pattern.
https://github.com/philanc/minisock useful? we could almost replace
nellie with it only not quite (it hardcodes 0 as the "protocol" param
to socket())
Fri Apr 19 20:55:22 BST 2024
We could have a service that's present only when a devdb entry is
present. For example mount_disk only runs when partlabel=foo
Or we could have a service that continues to run as the $somedatabase
service state changes and does different things depending on the
nature of those changes. For example, [I can't think of an example
now, but it was definitely an issue the other day, maybe I dreamt it]
I don't think this will be such an issue for devdb becuase there isn't
much in it that has continuously varying values. Maybe battery health
is the exception there
The step ahead we're thinking here is: how do clients do a request? A
single one-of request for state is fine but chances are that a client
will do that to get initial state and then need to open a netlink
socket to get updates: well, if we can feed them the initial state
filtered for their needs why can't we send them the relevant updates
as well? This makes the database server design a bit more complicated
as it needs to remember each client and their subscriptions, and then
send only relevant updates to each subscribed client
* should a client be allowed multiple subscriptions on the same
connection?
* do we guarantee that every message sent is matching the subscription
or can we send other stuff as well if it makes implementation easier?
it might defeat the purpose a bit because it means the client also
needs to filter, but the client will anyway have to do some message
parsing so they can distinguish add from remove
* where do we start?
Sun Apr 21 13:31:48 BST 2024
We have the mechanics of it working (albeit implemented in the
simplest possible terms), we need to glue it to some I/O
1) open a netlink socket and read the events from it
2) "create a PF_UNIX socket of type SOCK_STREAM, and accept connections on it, then each time you accept a connection, you get a new fd"
- accept connection
- read terms from it
- register callback that writes event to connected socket
minisock has no support for "test if fd is ready" or "wait for [fds]
to become ready", either we need poll() or we could add a call for "is
this fd ready to read" and use coroutines. Fork minisock or add as
another library?
[ if we fork minisock we could expose the protocol param to Lua
so we could use it for netlink ]
Tue Apr 23 19:13:45 BST 2024
we could convert from minisock to lualinux. if we can also use that to
get rid of nellie and/or lfs, the size tradeoff is minimal
---
Is there some way we could test the devout event loop?
I can register a fd with a callback
when the fd is ready, my callback is called
when the callback return true it remains registered
when the callback return true it is unregistered and the fd is closed
loop.register
loop.registered?
loop.feed
Tue Apr 23 20:34:03 BST 2024
I think we could make the event loop abstraction leak less?
It's not actually a _loop_, all the actual GOTO 10 happens
outside of it
[X] 1) see if we can do netlink in lualinux
[X] 2) if so, convert it to lualinux
[X] 3) add netlink socket to event loop
[X] 4) make it send messages to subscribers
5) package it
6) make uevent-watcher use it instead of netlink directly
7) write an inout test variant that has the stick inserted
at boot time already
I'm also thinking we could wrap the raw fds from lualinux into small
objects with read and close methods? It would make testing easier if
nothing else - also use of with-open. Maybe do that in anoia.
when a subscriber connects we need to send them their matching current
state before subscribing them [ needs a test ]
figure out what event format the subscribers want? lua-ish or send the
same messages as udev would? If we're going to send the originals,
should we store them alongside the parsed, or reconstruct from parsed?

View File

@ -4,6 +4,10 @@ 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 ''
@ -108,6 +112,7 @@ in {
tufted
iptables
usbutils
busybox
];
security.sudo.wheelNeedsPassword = false;
networking = {
@ -122,6 +127,7 @@ in {
isNormalUser = true;
uid = 1000;
extraGroups = [ "wheel"];
openssh.authorizedKeys.keys = cfg.keys;
};
services.getty.autologinUser = "liminix";
};

9
ci.nix
View File

@ -9,9 +9,14 @@ let
borderVmConf = ./bordervm.conf-example.nix;
inherit (pkgs.lib.attrsets) genAttrs;
devices = [
"gl-ar750" "gl-mt300n-v2" "gl-mt300a"
"qemu" "qemu-aarch64" "qemu-armv7l"
"gl-ar750"
"gl-mt300a"
"gl-mt300n-v2"
"qemu"
"qemu-aarch64"
"qemu-armv7l"
"tp-archer-ax23"
"zyxel-nwa50ax"
];
vanilla = ./vanilla-configuration.nix;
for-device = name:

View File

@ -1,8 +1,10 @@
{
device
deviceName ? null
, device ? (import ./devices/${deviceName} )
, liminix-config ? <liminix-config>
, nixpkgs ? <nixpkgs>
, borderVmConf ? ./bordervm.conf.nix
, imageType ? "primary"
}:
let
@ -19,17 +21,24 @@ let
});
eval = pkgs.lib.evalModules {
specialArgs = {
modulesPath = builtins.toString ./modules;
};
modules = [
{ _module.args = { inherit pkgs; inherit (pkgs) lim; }; }
./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;
@ -68,6 +77,7 @@ in {
min-copy-closure
fennelrepl
lzma
lua
];
};
}

View File

@ -73,7 +73,7 @@
MTK_INFRACFG = "y";
MTK_PMIC_WRAP = "y";
MTK_EFUSE="y";
NVMEM_MTK_EFUSE="y";
# MTK_HSDMA="y";
MTK_SCPSYS="y";
MTK_SCPSYS_PM_DOMAINS="y";
@ -92,7 +92,6 @@
MEDIATEK_GE_PHY = "y";
# MEDIATEK_MT6577_AUXADC = "y";
# MEDIATEK_WATCHDOG = "y";
NET_MEDIATEK_SOC = "y";
NET_MEDIATEK_SOC_WED = "y";
NET_MEDIATEK_STAR_EMAC = "y"; # this enables REGMAP_MMIO

View File

@ -110,13 +110,11 @@
ifname = "eth0.1";
primary = eth;
vid = "1";
dependencies = [swconfig eth];
};
wan = vlan.build {
ifname = "eth0.2";
primary = eth;
vid = "2";
dependencies = [swconfig eth];
};
wlan = link.build {
ifname = "wlan0";
@ -126,7 +124,8 @@
};
boot.tftp = {
loadAddress = lim.parseInt "0x00A00000";
};
appendDTB = true;
};
kernel = {
src = pkgs.fetchurl {
@ -136,6 +135,7 @@
};
extraPatchPhase = ''
${openwrt.applyPatches.ramips}
${openwrt.applyPatches.rt2x00}
'';
config = {

View File

@ -97,7 +97,7 @@
swconfig dev switch0 vlan 2 set ports '0 6t'
swconfig dev switch0 set apply
'';
down = "swconfig dev switch0 set reset";
down = "${pkgs.swconfig}/bin/swconfig dev switch0 set reset";
};
in rec {
eth = link.build { ifname = "eth0"; dependencies = [swconfig]; };
@ -122,6 +122,7 @@
# 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;
};
kernel = {

View File

@ -0,0 +1,155 @@
#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

@ -0,0 +1,155 @@
#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

@ -0,0 +1,367 @@
{
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

@ -0,0 +1,56 @@
#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

@ -28,34 +28,12 @@ in rec {
../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";
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;
@ -105,7 +83,7 @@ in rec {
};
services.mount_external_disk = svc.mount.build {
device = "LABEL=backup-disk";
partlabel = "backup-disk";
mountpoint = "/srv";
fstype = "ext4";
};
@ -159,5 +137,17 @@ in rec {
gid=500; usernames = ["backup"];
};
defaultProfile.packages = with pkgs; [e2fsprogs strace tcpdump ];
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;
};
})
];
}

View File

@ -158,7 +158,6 @@ in rec {
};
services.firewall = svc.firewall.build {
ruleset = import ./demo-firewall.nix;
};
services.packet_forwarding = svc.network.forward.build { };

View File

@ -8,12 +8,10 @@
config,
pkgs,
lib,
modulesPath,
...
}: let
secrets = import ./extneder-secrets.nix;
inherit (pkgs.liminix.services) oneshot longrun bundle target;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) dropbear ifwait serviceFns;
svc = config.system.service;
in rec {
boot = {
@ -24,113 +22,32 @@ in rec {
};
imports = [
../modules/wlan.nix
../modules/vlan
../modules/network
../modules/hostapd
../modules/bridge
../modules/ssh
"${modulesPath}/profiles/wap.nix"
"${modulesPath}/vlan"
"${modulesPath}/ssh"
];
hostname = "extneder";
kernel = {
config = {
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.
# we're not running a firewall, so why do we need
# nftables config?
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.hostap = 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.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; [
profile.wap = {
interfaces = with config.hardware.networkInterfaces; [
lan
wlan
];
};
services.sshd = svc.ssh.build {};
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
)
'';
};
filesystem = dir {
etc = dir {
"resolv.conf" = symlink "${services.resolvconf}/.outputs/resolv.conf";
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;
};
};
};
services.defaultroute4 = svc.network.route.build {
via = "$(output ${services.dhcpc} router)";
target = "default";
dependencies = [services.dhcpc];
};
services.sshd = svc.ssh.build {};
users.root.passwd = lib.mkForce secrets.root.passwd;
defaultProfile.packages = with pkgs; [nftables strace tcpdump swconfig];
}

120
examples/nwa50ax-ap.nix Normal file
View File

@ -0,0 +1,120 @@
{ 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

@ -6,23 +6,18 @@
# problems.
{ config, pkgs, lib, ... } :
{ config, pkgs, lib, modulesPath, ... } :
let
secrets = {
domainName = "fake.liminix.org";
firewallRules = {};
} // (import ./rotuer-secrets.nix);
inherit (pkgs.liminix.services) oneshot longrun bundle;
inherit (pkgs.liminix.services) oneshot bundle;
inherit (pkgs) 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;
};
@ -36,65 +31,62 @@ in rec {
};
imports = [
../modules/wlan.nix
../modules/network
../modules/ppp
../modules/dnsmasq
../modules/dhcp6c
../modules/firewall
../modules/hostapd
../modules/bridge
../modules/ntp
../modules/schnapps
../modules/ssh
../modules/outputs/btrfs.nix
../modules/outputs/extlinux.nix
"${modulesPath}/profiles/gateway.nix"
"${modulesPath}/schnapps"
"${modulesPath}/outputs/btrfs.nix"
"${modulesPath}/outputs/extlinux.nix"
];
hostname = "rotuer";
rootfsType = "btrfs";
rootOptions = "subvol=@";
boot.loader.extlinux.enable = true;
services.hostap = svc.hostapd.build {
interface = config.hardware.networkInterfaces.wlan;
params = {
ssid = secrets.ssid;
hw_mode="g";
channel = "2";
ieee80211n = 1;
} // wirelessConfig;
};
services.hostap5 = svc.hostapd.build {
interface = config.hardware.networkInterfaces.wlan5;
params = rec {
ssid = "${secrets.ssid}5";
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.int = svc.network.address.build {
interface = svc.bridge.primary.build { ifname = "int"; };
family = "inet"; address ="${secrets.lan.prefix}.1"; prefixLength = 24;
};
services.bridge = svc.bridge.members.build {
primary = services.int;
members = with config.hardware.networkInterfaces;
[ wlan
wlan5
lan0
lan1
lan2
lan3
lan4
];
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.ntp = svc.ntp.build {
@ -106,95 +98,6 @@ in rec {
users.root = secrets.root;
services.dns =
let interface = services.int;
in svc.dnsmasq.build {
resolvconf = services.resolvconf;
inherit interface;
ranges = [
"${secrets.lan.prefix}.10,${secrets.lan.prefix}.240"
# ra-stateless: 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.
"::,constructor:$(output ${interface} ifname),ra-stateless"
];
# You can add static addresses for the DHCP server here. I'm
# not putting my actual MAC addresses in a public git repo ...
hosts = { } // lib.optionalAttrs (builtins.pathExists ./static-leases.nix) (import ./static-leases.nix);
upstreams = [ "/${secrets.domainName}/" ];
domain = secrets.domainName;
};
services.wan = svc.pppoe.build {
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
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 {
ruleset =
let defaults = import ./demo-firewall.nix;
in lib.recursiveUpdate defaults secrets.firewallRules;
};
services.packet_forwarding = svc.network.forward.build { };
services.dhcp6c =
let client = svc.dhcp6c.client.build {
interface = services.wan;
};
in bundle {
name = "dhcp6c";
contents = [
(svc.dhcp6c.prefix.build {
inherit client;
interface = services.int;
})
(svc.dhcp6c.address.build {
inherit client;
interface = services.wan;
})
];
};
defaultProfile.packages = with pkgs; [
min-collect-garbage
nftables

View File

@ -9,28 +9,29 @@
./busybox.nix
./dhcp6c
./dnsmasq
./outputs/ext4fs.nix
./firewall
./hardware.nix
./hostapd
./hostname.nix
./outputs/initramfs.nix
./outputs/jffs2.nix
./kernel
./outputs/kexecboot.nix
./mdevd.nix
./mount
./network
./ntp
./outputs.nix
./outputs/vmroot.nix
./outputs/ubimage.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
./outputs/tftpboot.nix
./outputs/ubifs.nix
./users.nix
./vlan
./watchdog

View File

@ -12,9 +12,6 @@ let
type_service = pkgs.liminix.lib.types.service;
in {
imports = [
./kernel # kernel is a separate module for doc purposes
];
options = {
defaultProfile = {
packages = mkOption {
@ -29,6 +26,10 @@ in {
services = mkOption {
type = types.attrsOf type_service;
};
system.callService = mkOption {
type = types.functionTo (types.functionTo types.anything);
};
filesystem = mkOption {
type = types.anything;
description = ''
@ -58,6 +59,15 @@ in {
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";
@ -102,6 +112,31 @@ in {
"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/";

View File

@ -14,6 +14,8 @@ let
inherit (pkgs) liminix;
in
{
imports = [ ../ifwait ];
options = {
system.service.bridge = {
primary = mkOption { type = liminix.lib.types.serviceDefn; };
@ -27,7 +29,7 @@ in
description = "bridge interface name to create";
};
};
members = liminix.callService ./members.nix {
members = config.system.callService ./members.nix {
primary = mkOption {
type = liminix.lib.types.interface;
description = "primary bridge interface";
@ -47,5 +49,5 @@ in
# a better way to test for the existence of vlan config:
# maybe the module should set an `enabled` attribute?
BRIDGE_VLAN_FILTERING = "y";
};
};
}

View File

@ -2,6 +2,7 @@
liminix
, ifwait
, lib
, svc
}:
{ members, primary } :
@ -10,14 +11,20 @@ let
inherit (liminix.services) bundle oneshot;
inherit (lib) mkOption types;
addif = member :
oneshot {
name = "${primary.name}.member.${member.name}";
up = ''
dev=$(output ${member} ifname)
${ifwait}/bin/ifwait $dev running && ip link set dev $dev master $(output ${primary} ifname)
'';
down = "ip link set dev $(output ${member} ifname) nomaster";
# 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;
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";

View File

@ -32,23 +32,21 @@ 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" "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" "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"
];
in {
options = {

View File

@ -3,8 +3,9 @@
, linotify
, anoia
, lua
, lualinux
}:
writeFennel "acquire-delegated-prefix" {
packages = [ linotify anoia lua.pkgs.luafilesystem ];
packages = [ linotify anoia lualinux ];
mainFunction = "run";
} ./acquire-delegated-prefix.fnl

View File

@ -1,7 +1,8 @@
(local subject (require :acquire-wan-address))
(local { : view } (require :fennel))
(import-macros { : expect= } :anoia.assert)
(local { : merge : dup } (require :anoia))
;; nix-shell --run "cd modules/dhcp6c && fennelrepl acquire-wan-address-test.fnl"
(local a1
{
@ -47,19 +48,6 @@
}
)
(macro expect [assertion]
(let [msg (.. "expectation failed: " (view assertion))]
`(when (not ,assertion)
(assert false ,msg))))
(macro expect= [actual expected]
`(let [ve# (view ,expected)
va# (view ,actual)]
(when (not (= ve# va#))
(assert false
(.. "\nexpected " ve# "\ngot " va#)
))))
(fn first-address []
(let [deleted
(subject.deletions

View File

@ -2,9 +2,10 @@
writeFennel
, linotify
, anoia
, lualinux
, lua
}:
writeFennel "acquire-wan-address" {
packages = [ linotify anoia lua.pkgs.luafilesystem ];
packages = [ linotify anoia lualinux ];
mainFunction = "run";
} ./acquire-wan-address.fnl

View File

@ -56,8 +56,14 @@ in
config = {
system.service.firewall =
let svc = liminix.callService ./service.nix {
ruleset = mkOption {
extraRules = mkOption {
type = types.attrsOf types.attrs;
description = "firewall ruleset";
default = {};
};
rules = mkOption {
type = types.attrsOf types.attrs; # we could usefully tighten this a bit :-)
default = import ./default-rules.nix;
description = "firewall ruleset";
};
};
@ -68,13 +74,17 @@ in
};
in svc.build args' ;
};
programs.busybox.applets = [
"insmod" "rmmod"
];
kernel.config = {
NETFILTER = "y";
NETFILTER_ADVANCED = "y";
NETFILTER_NETLINK = "m";
NF_CONNTRACK = "m";
NETLINK_DIAG = "y";
IP6_NF_IPTABLES= "m";
IP_NF_IPTABLES = "m";
IP_NF_NAT = "m";

View File

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

View File

@ -67,6 +67,7 @@ in {
};
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"; };
radios = mkOption {
description = ''
Kernel modules (from mac80211 package) required for the

View File

@ -0,0 +1,18 @@
{ 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;
};
};
}

16
modules/ifwait/ifwait.nix Normal file
View File

@ -0,0 +1,16 @@
{ 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}
'';
}

27
modules/mdevd.nix Normal file
View File

@ -0,0 +1,27 @@
{ 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

@ -19,28 +19,39 @@ in {
type = liminix.lib.types.serviceDefn;
};
};
config.system.service = {
mount = liminix.callService ./service.nix {
device = mkOption {
type = types.str;
example = "/dev/sda1";
};
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";
};
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 = {

View File

@ -1,18 +1,26 @@
{
liminix
, uevent-watch
, lib
}:
{ device, mountpoint, options, fstype }:
{ partlabel, mountpoint, options, fstype }:
let
inherit (liminix.services) oneshot;
in oneshot {
name = "mount.${lib.escapeURL mountpoint}";
up = ''
while ! findfs ${device}; do
echo waiting for device ${device}
sleep 1
done
mount -t ${fstype} -o ${lib.concatStringsSep "," options} ${device} ${mountpoint}
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
'';
down = "umount ${mountpoint}";
}

View File

@ -111,7 +111,8 @@ in
};
uimage = liminix.builders.uimage {
commandLine = concatStringsSep " " config.boot.commandLine;
inherit (config.hardware) loadAddress entryPoint;
inherit (config.boot) commandLineDtbNode;
inherit (config.hardware) loadAddress entryPoint alignment;
inherit (config.boot) imageFormat;
inherit (o) kernel dtb;
};

View File

@ -122,7 +122,7 @@ in {
fdtput -p -t lx dtb /reserved-memory/$node reg $ac_prefix $(hex $rootfsStart) $sz_prefix $(hex $rootfsSize)
cmd="liminix ${cmdline} mtdparts=phram0:''${rootfsSize}(rootfs) phram.phram=phram0,''${rootfsStart},''${rootfsSize},${toString config.hardware.flash.eraseBlockSize} root=/dev/mtdblock0";
fdtput -t s dtb /chosen bootargs "$cmd"
fdtput -t s dtb /chosen ${config.boot.commandLineDtbNode} "$cmd"
dtbSize=$(binsize ./dtb )

View File

@ -12,9 +12,16 @@ in
imports = [
./initramfs.nix
];
options.system.outputs.rootubifs = mkOption {
type = types.package;
internal = true;
};
options.hardware.ubi = {
minIOSize = mkOption { type = types.str; };
eraseBlockSize = mkOption { type = types.str; }; # LEB
logicalEraseBlockSize = mkOption { type = types.str; }; # LEB
physicalEraseBlockSize = mkOption { type = types.str; }; # PEB
maxLEBcount = mkOption { type = types.str; }; # LEB
};
@ -26,7 +33,7 @@ in
};
boot.initramfs.enable = true;
system.outputs = {
rootfs =
rootubifs =
let
inherit (pkgs.pkgsBuildBuild) runCommand mtdutils;
cfg = config.hardware.ubi;
@ -35,7 +42,7 @@ in
} ''
mkdir tmp
tree=${o.bootablerootdir}
mkfs.ubifs -x favor_lzo -c ${cfg.maxLEBcount} -m ${cfg.minIOSize} -e ${cfg.eraseBlockSize} -y -r $tree --output $out --squash-uids -o $out
mkfs.ubifs -x favor_lzo -c ${cfg.maxLEBcount} -m ${cfg.minIOSize} -e ${cfg.logicalEraseBlockSize} -y -r $tree --output $out --squash-uids -o $out
'';
};
};

View File

@ -0,0 +1,91 @@
{
config
, pkgs
, lib
, ...
}:
let
inherit (pkgs) liminix;
inherit (lib) mkIf mkOption types concatStringsSep optionalString;
in
{
imports = [
./initramfs.nix
./ubifs.nix
];
options.hardware.ubi = {
minIOSize = mkOption { type = types.str; };
eraseBlockSize = mkOption { type = types.str; }; # LEB
maxLEBcount = mkOption { type = types.str; }; # LEB
};
config = mkIf (config.rootfsType == "ubifs") {
kernel.config = {
MTD_UBI="y";
UBIFS_FS = "y";
UBIFS_FS_SECURITY = "n";
};
boot.initramfs.enable = true;
system.outputs.rootfs =
let
inherit (pkgs.pkgsBuildBuild) runCommand;
ubiVolume = ({ name, volumeId, image, flags ? [] }:
''
[${name}]
mode=ubi
vol_id=${toString volumeId}
vol_type=dynamic
vol_name=${name}
vol_alignment=1
${optionalString (image != null) ''
image=${image}
''}
${optionalString (image == null) ''
vol_size=1MiB
''}
${optionalString (flags != []) ''
vol_flags=${concatStringsSep "," flags}
''}
'');
ubiImage = (volumes:
let
ubinizeConfig = pkgs.writeText "ubinize.conf" (concatStringsSep "\n" volumes);
inherit (pkgs.pkgsBuildBuild) mtdutils;
in
runCommand "ubinize" {
depsBuildBuild = [ mtdutils ];
# block size := 128kb
# page size := 2048
# ubninize opts := -E 5
} ''
ubinize -Q "$SOURCE_DATE_EPOCH" -o $out \
-p ${config.hardware.ubi.physicalEraseBlockSize} -m ${config.hardware.ubi.minIOSize} \
-e ${config.hardware.ubi.logicalEraseBlockSize} \
${ubinizeConfig}
'');
ubiDisk = ({ initramfs }:
let
initramfsUbi = ubiVolume {
name = "rootfs";
volumeId = 0;
image = initramfs;
flags = [ "autoresize" ];
};
in
ubiImage [
initramfsUbi
]);
disk = ubiDisk {
initramfs = config.system.outputs.rootubifs; # liminix.builders.squashfs config.filesystem.contents; # # assert this is a proper FIT.
};
in
disk;
};
}

View File

@ -0,0 +1,71 @@
{
config
, pkgs
, lib
, ...
}:
let
inherit (lib) mkIf mkEnableOption mkOption types concatStringsSep;
models = "6b e1 6f e1 ff ff ff ff ff ff";
in {
options.system.outputs = {
zyxel-nwa-fit = mkOption {
type = types.package;
description = ''
zyxel-nwa-fit
*************
This output provides a FIT image for Zyxel NWA series
containing a kernel image and an UBIFS rootfs.
It can usually be used as a factory image to install Liminix
on a system with pre-existing firmware and OS.
'';
};
};
imports = [
./ubivolume.nix
];
config = mkIf (config.rootfsType == "ubifs") {
system.outputs.zyxel-nwa-fit =
let
o = config.system.outputs;
# 8129kb padding.
paddedKernel = pkgs.runCommand "padded-kernel" {} ''
cp --no-preserve=mode ${o.uimage} $out
dd if=/dev/zero of=$out bs=1 count=1 seek=8388607
'';
firmwareImage = pkgs.runCommand "firmware-image" {} ''
cat ${paddedKernel} ${o.rootfs} > $out
'';
dts = pkgs.writeText "image.its" ''
/dts-v1/;
/ {
description = "Zyxel FIT (Flattened Image Tree)";
compat-models = [${models}];
#address-cells = <1>;
images {
firmware {
data = /incbin/("${firmwareImage}");
type = "firmware";
compression = "none";
hash@1 {
algo = "sha1";
};
};
};
};
'';
in
pkgs.runCommand "zyxel-nwa-fit-${config.boot.imageType}" {
nativeBuildInputs = [ pkgs.pkgsBuildBuild.ubootTools pkgs.pkgsBuildBuild.dtc ];
} ''
mkimage -f ${dts} $out
'';
};
}

View File

@ -0,0 +1,178 @@
{ config, pkgs, lib, ... } :
let
svc = config.system.service;
cfg = config.profile.gateway;
inherit (lib) mkOption mkEnableOption mkIf mdDoc types optional optionals;
inherit (pkgs) liminix serviceFns;
inherit (liminix.services) bundle oneshot;
hostaps =
let
defaults = {
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
};
in lib.mapAttrs'
(name : value :
let
attrs = defaults // { ssid = name; } // value;
in lib.nameValuePair
"hostap-${name}"
(svc.hostapd.build {
interface = attrs.interface;
params = lib.filterAttrs (k: v: k != "interface") attrs;
}))
cfg.wireless.networks;
in {
options.profile.gateway = {
lan = {
interfaces = mkOption {
type = types.listOf liminix.lib.types.interface;
default = [];
};
address = mkOption {
type = types.attrs;
};
prefix = mkOption { type = types.str; };
dhcp = {
start = mkOption { type = types.int; };
end = mkOption { type = types.int; };
hosts = mkOption { type = types.attrs; };
localDomain = mkOption { type = types.str; };
};
};
firewall = {
enable = mkEnableOption "firewall";
rules = mkOption { type = types.attrsOf types.attrs; };
};
wan = {
interface = mkOption { type = liminix.lib.types.interface; };
username = mkOption { type = types.str; };
password = mkOption { type = types.str; };
dhcp6.enable = mkOption { type = types.bool; };
};
wireless = mkOption {
type = types.attrsOf types.anything;
};
};
imports = [
../wlan.nix
../network
../ppp
../dnsmasq
../dhcp6c
../firewall
../hostapd
../bridge
../ntp
../ssh
{ config.services = hostaps; }
];
config = {
services.int = svc.network.address.build ({
interface = svc.bridge.primary.build { ifname = "int"; };
} // cfg.lan.address);
services.bridge = svc.bridge.members.build {
primary = config.services.int;
members = cfg.lan.interfaces;
};
services.wan = svc.pppoe.build {
inherit (cfg.wan) interface;
ppp-options = [
"debug" "+ipv6" "noauth"
"name" cfg.wan.username
"password" cfg.wan.password
];
};
services.packet_forwarding = svc.network.forward.build { };
services.dhcp6c =
let
client = svc.dhcp6c.client.build {
interface = config.services.wan;
};
bundl = bundle {
name = "dhcp6c";
contents = [
(svc.dhcp6c.prefix.build {
inherit client;
interface = config.services.int;
})
(svc.dhcp6c.address.build {
inherit client;
interface = config.services.wan;
})
];
};
in mkIf cfg.wan.dhcp6.enable bundl;
services.dns =
let interface = config.services.int;
dcfg = cfg.lan.dhcp;
in svc.dnsmasq.build {
resolvconf = config.services.resolvconf;
inherit interface;
ranges = [
"${cfg.lan.prefix}.${toString dcfg.start},${cfg.lan.prefix}.${toString dcfg.end}"
# ra-stateless: 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.
"::,constructor:$(output ${interface} ifname),ra-stateless"
];
hosts = dcfg.hosts;
upstreams = [ "/${dcfg.localDomain}/" ];
domain = dcfg.localDomain;
};
services.defaultroute4 = svc.network.route.build {
via = "$(output ${config.services.wan} address)";
target = "default";
dependencies = [ config.services.wan ];
};
services.defaultroute6 = svc.network.route.build {
via = "$(output ${config.services.wan} ipv6-peer-address)";
target = "default";
interface = config.services.wan;
};
services.firewall = mkIf cfg.firewall.enable
(svc.firewall.build {
extraRules = cfg.firewall.rules;
});
services.resolvconf = oneshot rec {
dependencies = [ config.services.wan ];
name = "resolvconf";
up = ''
. ${serviceFns}
( in_outputs ${name}
echo "nameserver $(output ${config.services.wan} ns1)" > resolv.conf
echo "nameserver $(output ${config.services.wan} ns2)" >> resolv.conf
chmod 0444 resolv.conf
)
'';
};
filesystem =
let inherit (pkgs.pseudofile) dir symlink;
in dir {
etc = dir {
"resolv.conf" = symlink "${config.services.resolvconf}/.outputs/resolv.conf";
};
};
};
}

98
modules/profiles/wap.nix Normal file
View File

@ -0,0 +1,98 @@
{
config,
pkgs,
lib,
...
}: let
inherit (pkgs) liminix;
inherit (lib) mkEnableOption mkOption types isDerivation hasAttr ;
inherit (pkgs.liminix.services) oneshot longrun bundle target;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) serviceFns;
svc = config.system.service;
cfg = config.profile.wap;
hostaps =
let
defaults = {
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
};
in lib.mapAttrs'
(name : value :
let
attrs = defaults // { ssid = name; } // value;
in lib.nameValuePair
"hostap-${name}"
(svc.hostapd.build {
interface = attrs.interface;
params = lib.filterAttrs (k: v: k != "interface") attrs;
}))
cfg.wireless.networks;
in {
imports = [
../wlan.nix
../network
../hostapd
../bridge
{ config.services = hostaps; }
];
options.profile.wap = {
interfaces = mkOption {
type = types.listOf liminix.lib.types.interface;
default = [];
};
wireless = mkOption {
type = types.attrsOf types.anything;
};
};
config = {
services.int = svc.bridge.primary.build {
ifname = "int";
};
services.bridge = svc.bridge.members.build {
primary = config.services.int;
members = cfg.interfaces;
};
services.dhcpc = svc.network.dhcp.client.build {
interface = config.services.int;
dependencies = [ config.services.hostname ];
};
services.defaultroute4 = svc.network.route.build {
via = "$(output ${config.services.dhcpc} router)";
target = "default";
dependencies = [config.services.dhcpc];
};
services.resolvconf = oneshot rec {
dependencies = [ config.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 ${config.services.dhcpc} dns); do
echo "nameserver $i" > resolv.conf
done
)
'';
};
filesystem = dir {
etc = dir {
"resolv.conf" = symlink "${config.services.resolvconf}/.outputs/resolv.conf";
};
};
};
}

View File

@ -34,7 +34,7 @@ fi
### If your services are managed by s6-rc:
### (replace /run/service with your scandir)
s6-rc-init /run/service -d -c /etc/s6-rc/compiled
s6-rc-init -d -c /etc/s6-rc/compiled /run/service
### 2. Starting the wanted set of services

View File

@ -15,4 +15,5 @@ in oneshot rec {
)
'';
down = "ip link set down dev ${ifname}";
dependencies = [ primary ];
}

View File

@ -1,6 +1,7 @@
{
liminix
, lib
, s6
}:
{ watched, headStart } :
let
@ -8,5 +9,5 @@ let
in longrun {
name = "watchdog";
run =
"HEADSTART=${toString headStart} ${./gaspode.sh} ${lib.concatStringsSep " " (builtins.map (s: s.name) watched)}";
"PATH=${s6}/bin:$PATH HEADSTART=${toString headStart} ${./gaspode.sh} ${lib.concatStringsSep " " (builtins.map (s: s.name) watched)}";
}

View File

@ -0,0 +1,60 @@
## Boot blessing via Zyxel
## =======================
## Boot blessing is the process to bless a particular boot configuration
## It is commonly encountered in devices with redundant partitions
## for automatic recovery of broken upgrades.
## This is also known as A/B schemas, where A represents the primary partition
## and B the secondary partition used for recovery.
## To use boot blessing on Liminix, you need to have the support of
## your bootloader to help you boot on the secondary partition in case of
## failure on the primary partition. The exact details are specifics to your device.
## See the Zyxel NWA50AX for an example.
## TODO: generalize this module.
{ config, lib, pkgs, ... }:
let
inherit (lib) mkOption types;
inherit (pkgs) liminix;
in
{
options.boot.zyxel-dual-image = mkOption {
type = liminix.lib.types.serviceDefn;
};
config.boot.zyxel-dual-image = liminix.callService ./service.nix {
ensureActiveImage = mkOption {
type = types.enum [ "primary" "secondary" ];
default = "primary";
description = ''At boot, ensure that the active image is the one specified.
If you are already on a broken image, you need to manually boot
into the right image via `atgo <image index>` in U-Boot.
'';
};
kernelCommandLineSource = mkOption {
type = types.enum [ "/proc/cmdline" "/proc/device-tree/chosen/bootargs" ];
default = "/proc/device-tree/chosen/bootargs";
description = ''Kernel command line arguments source file.
On MIPS, Liminix embeds the kernel command line in /proc/device-tree/chosen/bootargs-override.
In this instance, it does not get concatenated with `/proc/cmdline`.
Therefore you may prefer to source it from another place, like `/proc/device-tree/chosen/bootargs`.
'';
};
primaryMtdPartition = mkOption {
type = types.str;
description = "Primary MTD partition device node, i.e. for image 0.";
};
secondaryMtdPartition = mkOption {
type = types.str;
description = "Secondary MTD partition device node, i.e. for image 1.";
};
bootConfigurationMtdPartition = mkOption {
type = types.str;
description = "Boot configuration MTD partition device node.";
};
};
}

View File

@ -0,0 +1,33 @@
{
liminix
, lib
, zyxel-bootconfig
}:
{ ensureActiveImage, primaryMtdPartition, secondaryMtdPartition, bootConfigurationMtdPartition, kernelCommandLineSource }:
let
inherit (liminix.services) oneshot;
activeImageIndex = if ensureActiveImage == "primary" then 0 else 1;
in oneshot {
name = "zyxel-boot-configure";
up = ''
set -- $(cat /proc/device-tree/chosen/bootargs)
for x in "$@"; do
case "$x" in
bootImage=*)
BOOT_IMAGE="''${x#bootImage=}"
echo "Current boot image is $BOOT_IMAGE."
;;
esac
done
if test -z "$BOOT_IMAGE"; then
echo "No valid image was provided in the kernel command line."
exit 1
else
${lib.getExe zyxel-bootconfig} ${bootConfigurationMtdPartition} set-image-status "$BOOT_IMAGE" valid
${lib.getExe zyxel-bootconfig} ${bootConfigurationMtdPartition} set-active-image ${toString activeImageIndex}
echo "Active image is now ${ensureActiveImage}"
fi
'';
}

View File

@ -77,6 +77,22 @@ extraPkgs // {
};
# luarocks wants a cross-compiled cmake (which seems like a bug,
# we're never going to run luarocks on the device, but ...)
# but https://github.com/NixOS/nixpkgs/issues/284734
# so we do surgery on the cmake derivation until that's fixed
cmake = prev.cmake.overrideAttrs(o:
# don't override the build cmake or we'll have to rebuild
# half the known universe to no useful benefit
if final.stdenv.buildPlatform != final.stdenv.hostPlatform
then {
preConfigure =
builtins.replaceStrings
["$configureFlags"] ["$configureFlags $cmakeFlags"] o.preConfigure;
}
else {}
);
dnsmasq =
let d = prev.dnsmasq.overrideAttrs(o: {

14
pkgs/anoia/Makefile Normal file
View File

@ -0,0 +1,14 @@
default: fs.lua init.lua nl.lua svc.lua net/constants.lua
test:
ln -s . anoia
fennel test.fnl
fennel test-svc.fnl
net/constants.lua: net/constants.c
$(CC) -imacros sys/socket.h -imacros linux/netlink.h -E -P - < net/constants.c | sed 's/ *$$//g' | cat -s > net/constants.lua
%.lua: %.fnl
fennel --compile $< > $@

21
pkgs/anoia/assert.fnl Normal file
View File

@ -0,0 +1,21 @@
;; these are macros; this module should be imported
;; using import-macros
;; e.g. (import-macros { : expect= } :anoia.assert)
(fn expect [assertion]
(let [msg (.. "expectation failed: " (view assertion))]
`(when (not ,assertion)
(assert false ,msg))))
(fn expect= [actual expected]
`(let [view# (. (require :fennel) :view)
ve# (view# ,expected)
va# (view# ,actual)]
(when (not (= ve# va#))
(assert false
(.. "\nexpected " ve# "\ngot " va#)
))))
{ : expect : expect= }

View File

@ -1,22 +1,27 @@
{
fennel
, stdenv
, linotify
, lua
, lualinux
, cpio
}:
let pname = "anoia";
in stdenv.mkDerivation {
inherit pname;
version = "0.1";
src = ./.;
nativeBuildInputs = [ fennel ];
buildInputs = with lua.pkgs; [ luafilesystem ];
buildPhase = ''
for f in *.fnl ; do
fennel --compile $f > `basename $f .fnl`.lua
done
'';
nativeBuildInputs = [ fennel cpio ];
buildInputs = with lua.pkgs; [ linotify lualinux ];
outputs = [ "out" "dev" ];
doCheck = true;
installPhase = ''
mkdir -p "$out/share/lua/${lua.luaversion}/${pname}"
cp *.lua "$out/share/lua/${lua.luaversion}/${pname}"
find . -name \*.lua | cpio -p -d "$out/share/lua/${lua.luaversion}/${pname}"
mkdir -p "$dev/share/lua/${lua.luaversion}/${pname}"
cp assert.fnl "$dev/share/lua/${lua.luaversion}/${pname}"
'';
}

View File

@ -1,7 +1,30 @@
(local lfs (require :lfs))
(local ll (require :lualinux))
(local S_IFMT 0xf000)
(local S_IFSOCK 0xc000)
(local S_IFLNK 0xa000)
(local S_IFREG 0x8000)
(local S_IFBLK 0x6000)
(local S_IFDIR 0x4000)
(local S_IFCHR 0x2000)
(local S_IFIFO 0x1000)
(fn ifmt-bits [mode] (and mode (band mode 0xf000)))
(fn file-type [pathname]
(. {
S_IFDIR :directory
S_IFSOCK :socket
S_IFLNK :link
S_IFREG :file
S_IFBLK :block-device
S_IFCHR :character-device
S_IFIFO :fifo
}
(ifmt-bits (ll.lstat3 pathname))))
(fn directory? [pathname]
(= (lfs.symlinkattributes pathname :mode) "directory"))
(= (file-type pathname) :directory))
(fn mktree [pathname]
(if (or (= pathname "") (= pathname "/"))
@ -10,27 +33,37 @@
(or (directory? pathname)
(let [parent (string.gsub pathname "/[^/]+/?$" "")]
(or (directory? parent) (mktree parent))
(assert (lfs.mkdir pathname)))))
(assert (ll.mkdir pathname)))))
(fn dir [name]
(let [dp (assert (ll.opendir name) name)]
(fn []
(match (ll.readdir dp)
(name type) (values name type)
(nil err) (do (if err (print err)) (ll.closedir dp) nil)))))
(fn rmtree [pathname]
(case (lfs.symlinkattributes pathname)
(case (file-type pathname)
nil true
{:mode "directory"}
:directory
(do
(each [f (lfs.dir pathname)]
(each [f (dir pathname)]
(when (not (or (= f ".") (= f "..")))
(rmtree ( .. pathname "/" f)))
(lfs.rmdir pathname)))
{:mode "file"}
(ll.rmdir pathname)))
:file
(os.remove pathname)
{:mode "link"}
:link
(os.remove pathname)
unknown
(error (.. "can't remove " pathname " of kind \"" unknown.mode "\""))))
(error (.. "can't remove " pathname " of mode \"" unknown "\""))))
{
: mktree
: rmtree
: directory?
: dir
: file-type
:symlink (fn [from to] (ll.symlink from to))
}

View File

@ -1,3 +1,7 @@
(fn assoc [tbl k v]
(tset tbl k v)
tbl)
(fn merge [table1 table2]
(collect [k v (pairs table2) &into table1]
k v))
@ -14,9 +18,15 @@
f (do (f:close) true)
_ false))
(fn basename [path]
(string.match path ".*/([^/]-)$"))
(fn dirname [path]
(string.match path "(.*)/[^/]-$"))
(fn system [s]
(match (os.execute s)
res res
res (do (print (.. "Executed \"" s "\", exit code " (tostring res))) res)
(nil err) (error (.. "Error executing \"" s "\" (" err ")"))))
(fn hash [str]
@ -62,4 +72,15 @@
(s:sub 1 (- (# s) pad))))
{ : merge : split : file-exists? : system : hash : base64url : dup }
{
: assoc
: base64url
: basename
: dirname
: dup
: file-exists?
: hash
: merge
: split
: system
}

View File

@ -0,0 +1,11 @@
#define MACRO(c) [#c] = c,
return {
MACRO(SOCK_STREAM)
MACRO(SOCK_DGRAM)
MACRO(SOCK_RAW)
MACRO(AF_LOCAL)
MACRO(AF_NETLINK)
MACRO(NETLINK_KOBJECT_UEVENT)
}

15
pkgs/anoia/nl.fnl Normal file
View File

@ -0,0 +1,15 @@
(local netlink (require :netlink))
; (local { : view } (require :fennel))
(fn events [groups]
(let [sock (netlink.socket)]
(coroutine.wrap
(fn []
(each [_ e (ipairs (sock:query groups))]
(coroutine.yield e))
(while (sock:poll)
(each [_ e (ipairs (sock:event))]
(coroutine.yield e)))))))
{ : events }

View File

@ -1,7 +1,6 @@
(local inotify (require :inotify))
(local { : file-exists? } (require :anoia))
(local { : directory? } (require :anoia.fs))
(local lfs (require :lfs))
(local { : file-type : dir &as fs } (require :anoia.fs))
(fn read-line [name]
(with-open [f (assert (io.open name :r) (.. "can't open file " name))]
@ -20,15 +19,15 @@
handle))
(fn read-value [pathname]
(case (lfs.symlinkattributes pathname)
(case (file-type pathname)
nil nil
{:mode "directory"}
(collect [f (lfs.dir pathname)]
:directory
(collect [f (fs.dir pathname)]
(when (not (or (= f ".") (= f "..")))
(values f (read-value ( .. pathname "/" f)))))
{:mode "file"}
:file
(read-line pathname)
{:mode "link"}
:link
(read-line pathname)
unknown
(error (.. "can't read " pathname " of kind \"" unknown.mode "\""))))

7
pkgs/anoia/test-nl.fnl Normal file
View File

@ -0,0 +1,7 @@
(local nl (require :anoia.nl))
(local { : view } (require :fennel))
(let [events (nl.events {:link true})]
(each [ev events]
(print "got one ")
(print (view ev))))

View File

@ -1,4 +1,4 @@
(local svc (require :anoia.svc))
(local svc (require :svc))
(local { : view } (require :fennel))
(local ex (svc.open "./example-output"))

View File

@ -1,9 +1,10 @@
(local { : hash : base64url } (require :anoia))
(local { : hash : base64url } (require :init))
(import-macros { : expect= } :assert)
(assert (= (hash "") 5381))
(expect= (hash "") 5381)
;; these examples from https://theartincode.stanis.me/008-djb2/
(assert (= (hash "Hello") 210676686969))
(assert (= (hash "Hello!") 6952330670010))
(expect= (hash "Hello") 210676686969)
(expect= (hash "Hello!") 6952330670010)
(assert (= (base64url "hello world") "aGVsbG8gd29ybGQ"))
(expect= (base64url "hello world") "aGVsbG8gd29ybGQ")

View File

@ -56,6 +56,7 @@ in {
# please keep the rest of this list alphabetised :-)
anoia = callPackage ./anoia {};
devout = callPackage ./devout {};
fennel = callPackage ./fennel {};
fennelrepl = callPackage ./fennelrepl {};
firewallgen = callPackage ./firewallgen {};
@ -70,6 +71,7 @@ in {
levitate = callPackage ./levitate {};
libubootenv = callPackage ./libubootenv {};
linotify = callPackage ./linotify {};
lualinux = callPackage ./lualinux {};
# we need to build real lzma instead of using xz, because the lzma
# decoder in u-boot doesn't understand streaming lzma archives
@ -79,8 +81,11 @@ in {
lzma = callPackage ./lzma {};
mac80211 = callPackage ./mac80211 {};
zyxel-bootconfig = callPackage ./zyxel-bootconfig {};
min-collect-garbage = callPackage ./min-collect-garbage {};
min-copy-closure = callPackage ./min-copy-closure {};
minisock = callPackage ./minisock {};
nellie = callPackage ./nellie {};
netlink-lua = callPackage ./netlink-lua {};
odhcp-script = callPackage ./odhcp-script {};
odhcp6c = callPackage ./odhcp6c {};
@ -104,6 +109,7 @@ in {
swconfig = callPackage ./swconfig {};
systemconfig = callPackage ./systemconfig {};
tufted = callPackage ./tufted {};
uevent-watch = callPackage ./uevent-watch {};
writeAshScript = callPackage ./write-ash-script {};
writeFennel = callPackage ./write-fennel {};
writeFennelScript = callPackage ./write-fennel-script {};

26
pkgs/devout/default.nix Normal file
View File

@ -0,0 +1,26 @@
{
lua
, nellie
, writeFennel
, anoia
, fennel
, stdenv
, fennelrepl
, lualinux
}:
stdenv.mkDerivation {
name = "devout";
src = ./.;
nativeBuildInputs = [ fennelrepl ];
postBuild = ''
LUA_CPATH=${lualinux}/lib/lua/5.3/?.so\;$LUA_CPATH \
fennelrepl ./test.fnl
'';
installPhase = ''
mkdir -p $out/bin
cp -p ${writeFennel "devout" {
packages = [fennel anoia nellie lualinux];
mainFunction = "run";
} ./devout.fnl} $out/bin/devout
'';
}

173
pkgs/devout/devout.fnl Normal file
View File

@ -0,0 +1,173 @@
(local ll (require :lualinux))
(local {
: AF_LOCAL
: AF_NETLINK
: SOCK_STREAM
: SOCK_RAW
: NETLINK_KOBJECT_UEVENT
} (require :anoia.net.constants))
(local { : view } (require :fennel))
(fn trace [expr]
(do (print :TRACE (view expr)) expr))
(macro check-errno [expr]
(let [{ :view v } (require :fennel)]
`(case ,expr
val# val#
(nil err#) (error (string.format "%s failed: errno=%d" ,(v expr) err#)))))
(fn format-event [e]
(..
(string.format "%s@%s\0" e.action e.path)
(table.concat
(icollect [k v (pairs e.attributes)]
(string.format "%s=%s" (string.upper k) v ))
"\n")))
(fn event-matches? [e terms]
(accumulate [match? true
name value (pairs terms)]
(and match? (= value (. e.attributes name)))))
(fn parse-event [s]
(let [at (string.find s "@" 1 true)
(nl nxt) (string.find s "\0" 1 true)
attributes
(collect [k v (string.gmatch
(string.sub s (+ 1 nxt))
"(%g-)=(%g+)")]
(k:lower) v)]
{ : attributes
:path (string.sub s (+ at 1) (- nl 1))
:action (string.sub s 1 (- at 1))
:format format-event
:matches? event-matches?
}))
(fn find-in-database [db terms]
(accumulate [found []
_ e (pairs db)]
(if (e:matches? terms)
(doto found (table.insert e))
found)))
(fn record-event [db subscribers str]
(let [e (parse-event str)]
(match e.action
:add (tset db e.path e)
:change (tset db e.path e)
;; should we do something for bind?
:remove (tset db e.path nil)
)
(each [_ { : terms : callback } (pairs subscribers)]
(if (e:matches? terms) (callback e)))
e))
(fn database []
(let [db {}
subscribers []]
{
:find (fn [_ terms] (find-in-database db terms))
:add (fn [_ event-string] (when event-string (record-event db subscribers event-string)))
:at-path (fn [_ path] (. db path))
:subscribe (fn [_ id callback terms]
(let [past-events (find-in-database db terms)]
(each [_ e (pairs past-events)]
(callback e)))
(tset subscribers id {: callback : terms }))
:unsubscribe (fn [_ id] (tset subscribers id nil))
}))
;; grepped from kernel headers
(local POLLIN 0x0001)
(local POLLPRI 0x0002)
(local POLLOUT 0x0004)
(local POLLERR 0x0008)
(local POLLHUP 0x0010)
(local POLLNVAL 0x0020)
(fn unix-socket [name]
(let [addr (string.pack "=Hz" AF_LOCAL name)]
(case (ll.socket AF_LOCAL SOCK_STREAM 0)
fd (case (ll.bind fd addr)
0 (doto fd (ll.listen 32))
(nil err) (values nil err))
(nil err) (values nil err))))
(fn pollfds-for [fds]
(icollect [_ v (ipairs fds)]
(bor (lshift v 32) (lshift 1 16))))
(fn unpack-pollfds [pollfds]
(collect [_ v (ipairs pollfds)]
(let [fd (band (rshift v 32) 0xffffffff)
revent (band v 0xffff)]
(values fd (if (> revent 0) revent nil)))))
(fn parse-terms [str]
(collect [n (string.gmatch (str:gsub "\n+$" "") "([^ ]+)")]
(string.match n "(.-)=(.+)")))
(fn handle-client [db client]
(match (ll.read client)
"" (do
(db:unsubscribe client)
false)
s (do
(db:subscribe
client
(fn [e]
(ll.write client (format-event e)))
(parse-terms s))
true)
(nil err) (do (print err) false)))
(fn open-netlink [groups]
(match (ll.socket AF_NETLINK SOCK_RAW NETLINK_KOBJECT_UEVENT)
fd (doto fd (ll.bind (string.pack "I2I2I4I4" ; family pad pid groups
AF_NETLINK 0 0 groups)))
(nil errno) (values nil errno)))
(fn event-loop []
(let [fds {}]
{
:register #(tset fds $2 $3)
:feed (fn [_ revents]
(each [fd revent (pairs revents)]
(when (not ((. fds fd) fd))
(tset fds fd nil)
(ll.close fd))))
:fds #(icollect [fd _ (pairs fds)] fd)
:_tbl #(do fds) ;exposed for tests
}))
(fn run []
(let [[sockname nl-groups] arg
s (check-errno (unix-socket sockname))
db (database)
nl (check-errno (open-netlink nl-groups))
loop (event-loop)]
(loop:register
s
#(case
(ll.accept s)
(client addr)
(do
(loop:register client (partial handle-client db))
true)
(nil err)
(print (string.format "error accepting connection, errno=%d" err))))
(loop:register
nl
#(do (db:add (ll.read nl)) true))
(ll.write 10 "ready\n")
(while true
(let [pollfds (pollfds-for (loop:fds))]
(ll.poll pollfds 5000)
(loop:feed (unpack-pollfds pollfds))))))
{ : database : run : event-loop : parse-event }

210
pkgs/devout/test.fnl Normal file
View File

@ -0,0 +1,210 @@
(local { : database : event-loop : parse-event } (require :devout))
(local { : view } (require :fennel))
(local ll (require :lualinux))
(import-macros { : expect : expect= } :anoia.assert)
(var failed false)
(fn fail [d msg] (set failed true) (print :FAIL d (.. "\n" msg)))
(macro example [description & body]
(if (. body 1)
`(let [(ok?# err#) (xpcall (fn [] ,body) debug.traceback)]
(if ok?#
(print :PASS ,description)
(fail ,description err#)))
`(print :PENDING ,description)))
(local sda-uevent
"add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda\0ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda
SUBSYSTEM=block
MAJOR=8
MINOR=0
DEVNAME=sda
DEVTYPE=disk
DISKSEQ=2
SEQNUM=1527")
(local sdb1-insert
"add@/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1\0ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1
SUBSYSTEM=block
DEVNAME=/dev/sdb1
DEVTYPE=partition
DISKSEQ=33
PARTN=1
SEQNUM=82381
MAJOR=8
MINOR=17")
(local sdb1-remove
"remove@/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1\0ACTION=remove
DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1
SUBSYSTEM=block
DEVNAME=/dev/sdb1
DEVTYPE=partition
DISKSEQ=33
PARTN=1
SEQNUM=82386
MAJOR=8
MINOR=17")
(example
"I can parse an event"
(let [e (parse-event sdb1-insert)]
(expect= e.attributes.seqnum "82381")
(expect= e.attributes.devname "/dev/sdb1")
(expect= e.path "/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1")
(expect= e.action :add)
(expect= e (parse-event (e:format)))))
(example
"An event can match against terms"
(let [terms {:devname "foo" :partname "my-usbstick"}]
(expect= (: (parse-event "add@/\0SEQNUM=1") :matches? terms) false)
(expect= (: (parse-event "add@/\0DEVNAME=bill") :matches? terms) false)
(expect= (: (parse-event "add@/\0DEVNAME=foo\nPARTNAME=my-usbstick") :matches? terms) true)
(expect= (: (parse-event "add@/\0DEVNAME=foo\nPARTNAME=my-usbstick\nOTHERTHING=bar") :matches? terms) true)
))
(example
"given an empty database, searching it finds no entries"
(let [db (database)]
(expect= (db:find {:partname "boot"}) [])))
(example
"when I add a device, I can find it"
(let [db (database)]
(db:add sda-uevent)
(let [[m & more] (db:find {:devname "sda"})]
(expect= m.attributes.devname "sda")
(expect= m.attributes.major "8")
(expect= more []))))
(example
"when I add a device, I cannot find it with wrong terms"
(let [db (database)]
(db:add sda-uevent)
(expect= (db:find {:devname "sdb"}) [])))
(example
"when I add a device, I can retrieve it by path"
(let [db (database)]
(db:add sda-uevent)
(let [m (db:at-path "/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda")]
(expect= m.attributes.devname "sda")
(expect= m.attributes.major "8"))))
(example
"when I add and then remove a device, I cannot retrieve it by path"
(let [db (database)]
(db:add sdb1-insert)
(db:add sdb1-remove)
(expect= (db:at-path "/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1") nil)))
(example
"when I add and then remove a device, I cannot find it"
(let [db (database)]
(db:add sdb1-insert)
(db:add sda-uevent)
(db:add sdb1-remove)
(expect= (db:find {:devname "/dev/sdb1"}) [])))
(example
"when I search on multiple terms it uses all of them"
(let [db (database)]
(db:add sda-uevent)
(expect= (# (db:find {:devname "sda" :devtype "disk"})) 1)
(expect= (# (db:find {:devname "sda" :devtype "dosk"})) 0)))
;;; tests for indices
(example "when I add a device with $attributes major minor foo bar baz,
it is added to indices for foo bar baz but not major minor")
(example "a removed device can no longer be found by looking in any index")
(example "when I query with multiple attributes, the search is performed using the most specific attribute"
;; (= the attribute whose
;; value at this key has fewest elements)
)
;;; tests for subscriptions
(example
"I can subscribe to some search terms and be notified of matching events"
(var received [])
(let [db (database)
subscriber (fn [e] (table.insert received e))]
(db:subscribe :me subscriber {:devname "/dev/sdb1"})
(db:add sdb1-insert)
(db:add sda-uevent)
(db:add sdb1-remove)
(expect= (# received) 2)))
(example
"Subscribers get notifications of prior events for present devices"
(var received [])
(let [db (database)
subscriber (fn [e] (table.insert received e))]
(db:add sdb1-insert)
(db:add sda-uevent)
(db:subscribe :me subscriber {:devname "/dev/sdb1"})
(expect= (# received) 1)))
(example
"I can unsubscribe after subscribing"
(var received [])
(let [db (database)
subscriber (fn [e] (table.insert received e))]
(db:subscribe :me subscriber {:devname "/dev/sdb1"})
(db:unsubscribe :me)
(db:add sdb1-insert)
(db:add sda-uevent)
(db:add sdb1-remove)
(expect= (# received) 0)))
;;; test for event loop
(example
"I can register a fd with a callback"
(let [loop (event-loop)
cb #(print $1)]
(loop:register 3 cb)
(expect= (. (loop:_tbl) 3) cb)))
(example
"when the fd is ready, my callback is called"
(let [loop (event-loop)]
(var ran? false)
(loop:register 3 #(set ran? true))
(loop:feed {3 1})
(expect= ran? true)
))
(example
"when the callback returns true it remains registered"
(let [loop (event-loop)]
(loop:register 3 #true)
(loop:feed {3 1})
(expect (. (loop:_tbl) 3))
))
(fn new-fd []
(ll.open "/dev/zero" 0 0x1ff))
(example
"when the callback returns false it is unregistered and the fd is closed"
(let [loop (event-loop)
fd (new-fd)]
(expect (> fd 2))
(loop:register 3 #false)
(loop:feed {3 1})
(expect (not (. (loop:_tbl) 3)))
(assert (not (os.execute (string.format "test -e /dev/fd/%d" fd))))
))
(if failed (os.exit 1) (print "OK"))

View File

@ -5,16 +5,20 @@
, lib
, luaPackages
, lua
, lualinux
, writeScriptBin
, linotify
, anoia
, netlink-lua
, fennel
}:
let packages = [
linotify
anoia
fennel
lua.pkgs.luafilesystem
lualinux
netlink-lua
lua.pkgs.readline
];
join = ps: builtins.concatStringsSep ";" ps;
luapath = join (builtins.map (f:
@ -29,6 +33,7 @@ in writeScriptBin "fennelrepl" ''
package.cpath = ${lib.strings.escapeShellArg luacpath} .. ";" .. (package.cpath or "")
local fennel = require "fennel"
table.insert(package.loaders or package.searchers,1, fennel.searcher)
fennel['macro-path'] = "${anoia.dev}/share/lua/${lua.luaversion}/?.fnl;" .. fennel['macro-path']
local more_fennel = os.getenv("FENNEL_PATH")
if more_fennel then

View File

@ -3,8 +3,9 @@
, netlink-lua
, writeFennelScript
, runCommand
, anoia
}:
runCommand "ifwait" {} ''
mkdir -p $out/bin
cp -p ${writeFennelScript "ifwait" [netlink-lua] ./ifwait.fnl} $out/bin/ifwait
cp -p ${writeFennelScript "ifwait" [anoia netlink-lua] ./ifwait.fnl} $out/bin/ifwait
''

195
pkgs/ifwait/events-fixture Normal file
View File

@ -0,0 +1,195 @@
{:event "newlink"
:hwaddr "00:00:00:00:00:00"
:index 1
:mtu 65536
:name "lo"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "50:3e:aa:08:df:52"
:index 2
:mtu 1500
:name "enp1s0"
:running "no"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "1c:1b:0d:9c:39:2d"
:index 3
:mtu 1500
:name "enp0s31f6"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "da:4d:53:c3:54:43"
:index 4
:mtu 1500
:name "vbridge0"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "00:28:f8:69:fa:14"
:index 6
:mtu 1500
:name "wlp4s0"
:running "no"
:stamp 857161382
:up "no"}
{:event "newlink"
:hwaddr "02:42:b1:e6:e5:bd"
:index 7
:mtu 1500
:name "br-7ddfef4820c5"
:running "no"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "02:42:8d:d4:36:34"
:index 8
:mtu 1500
:name "br-95da8b40a7cc"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "02:42:bc:cf:a8:5e"
:index 9
:mtu 1500
:name "docker0"
:running "no"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "b6:66:50:69:33:a6"
:index 11
:mtu 1500
:name "veth2ff6ec3"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "e6:94:c8:48:f3:97"
:index 13
:mtu 1500
:name "veth0913974"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "9a:87:d8:f2:c6:96"
:index 15
:mtu 1500
:name "veth0e74156"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "5e:d2:92:b9:5f:6d"
:index 17
:mtu 1500
:name "veth89a36b3"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "ca:88:3f:09:bc:51"
:index 19
:mtu 1500
:name "veth73c1e0b"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newlink"
:hwaddr "b6:7d:5c:38:89:1d"
:index 21
:mtu 1500
:name "dummy0"
:running "no"
:stamp 857161382
:up "no"}
{:event "newlink"
:hwaddr "52:f0:46:da:0c:0c"
:index 22
:mtu 1500
:name "dummy1"
:running "yes"
:stamp 857161382
:up "yes"}
{:event "newneigh"
:hwaddr "00:22:61:3d:f7:54"
:index 4
:ip "192.168.8.140"
:probes 1
:stamp 857165355
:state "stale"}
{:event "delneigh"
:hwaddr "5c:60:ba:58:34:93"
:index 3
:stamp 857166891
:state "stale"}
{:event "newneigh"
:hwaddr "80:64:6f:9e:15:02"
:index 4
:ip "192.168.8.161"
:probes 1
:stamp 857172523
:state "stale"}
{:event "newneigh"
:hwaddr "e4:95:6e:42:c2:6c"
:index 3
:stamp 857174763
:state "reachable"}
{:event "newneigh"
:hwaddr "e4:b3:18:76:1b:23"
:index 4
:ip "2001:8b0:de3a:40de:4708:c700:4de2:9264"
:probes 1
:stamp 857175595
:state "stale"}
{:event "newneigh"
:hwaddr "80:64:6f:9e:10:c6"
:index 4
:ip "192.168.8.53"
:probes 1
:stamp 857176619
:state "stale"}
{:event "newneigh"
:hwaddr "80:64:6f:9e:15:02"
:index 4
:ip "192.168.8.161"
:probes 1
:stamp 857177643
:state "probe"}
{:event "newneigh"
:hwaddr "80:64:6f:9e:15:02"
:index 4
:ip "192.168.8.161"
:probes 1
:stamp 857177644
:state "reachable"}
{:event "newlink"
:hwaddr "b6:7d:5c:38:89:1d"
:index 21
:mtu 1500
:name "dummy0"
:running "yes"
:stamp 857178258
:up "yes"}
{:event "newlink"
:hwaddr "b6:7d:5c:38:89:1d"
:index 21
:mtu 1500
:name "dummy0"
:running "no"
:stamp 857181661
:up "no"}
{:event "newneigh"
:hwaddr "80:64:6f:9e:10:c6"
:index 4
:ip "192.168.8.53"
:probes 1
:stamp 857182251
:state "probe"}

View File

@ -1,52 +1,64 @@
(local netlink (require :netlink))
(local sock (netlink.socket))
(local nl (require :anoia.nl))
(local { : assoc : system } (require :anoia))
; (local { : view} (require :fennel))
(fn assoc [tbl k v]
(tset tbl k v)
tbl)
(fn parse-args [args]
(match args
["-v" & rest] (assoc (parse-args rest) :verbose true)
["-t" timeout & rest] (assoc (parse-args rest) :timeout (tonumber timeout))
["-s" service & rest] (assoc (parse-args rest) :service service)
[linkname "up"] {:link linkname :expecting "up"}
[linkname "running"] {:link linkname :expecting "running"}
[linkname "present"] {:link linkname :expecting "present"}
[linkname nil] {:link linkname :expecting "present"}
_ nil))
(local parameters
(or
(parse-args arg)
(assert false (.. "Usage: " (. arg 0) " [-v] ifname [present|up|running]"))))
(fn event-matches? [params v]
(let [got
(match v
;; - up: Reflects the administrative state of the interface (IFF_UP)
;; - running: Reflects the operational state (IFF_RUNNING).
{:event "newlink" :name params.link :up :yes :running :yes}
{:present true :up true :running true}
(fn run-events [evs]
(each [_ v (ipairs evs)]
(let [got
(match v
;; - up: Reflects the administrative state of the interface (IFF_UP)
;; - running: Reflects the operational state (IFF_RUNNING).
{:event "newlink" :name parameters.link :up :yes :running :yes}
{:present true :up true :running true}
{:event "newlink" :name params.link :up :yes}
{:present :true :up true}
{:event "newlink" :name parameters.link :up :yes}
{:present :true :up true}
{:event "newlink" :name params.link}
{:present true }
{:event "newlink" :name parameters.link}
{:present true }
_
{})]
(not (not (. got params.expecting)))))
_
{})]
(when (. got parameters.expecting)
(os.exit 0)))))
(var up :unknown)
(fn toggle-service [service wanted?]
(when (not (= up wanted?))
(set up
(if wanted?
(pcall system (.. "s6-rc -b -u change " service))
(not (pcall system (.. "s6-rc -b -d change " service)))))
))
(fn run [args event-fn]
(set up :unknown)
(let [parameters
(assert (parse-args args)
(.. "Usage: ifwait [-v] ifname [present|up|running]"))]
(when parameters.verbose
(print (.. "ifwait: waiting for "
parameters.link " to be " parameters.expecting)))
(when parameters.verbose
(print (.. (. arg 0) ": waiting for "
parameters.link " to be " parameters.expecting)))
(if parameters.service
(each [e (event-fn)]
(if (= e.name parameters.link)
(toggle-service parameters.service (event-matches? parameters e))))
(each [e (event-fn)
&until (event-matches? parameters e)]
true))))
(run-events (sock:query {:link true}))
(when (not (= (. arg 0) "test"))
(run arg #(nl.events {:link true})))
(while (sock:poll) (run-events (sock:event)))
{ : run }

117
pkgs/ifwait/test-ifwait.fnl Normal file
View File

@ -0,0 +1,117 @@
(local { : view &as fennel } (require :fennel))
(local anoia (require :anoia))
(import-macros { : expect= } :anoia.assert)
;; nix-shell --run "cd pkgs/ifwait && fennelrepl test-ifwait.fnl"
(var fake-system (fn [s] (print "executing " s)))
(tset anoia :system #(fake-system $1))
(fn event-generator [events]
(coroutine.wrap
(fn []
(each [_ e (ipairs events)] (coroutine.yield e)))))
(fn file-events [path]
(let [data (with-open [e (io.open path "r")] (e:read "*a"))
parse (fennel.parser data)]
(icollect [_ ast parse]
ast)))
(set _G.arg (doto [] (tset 0 "test")))
(local ifwait (require :ifwait))
(let [gen (event-generator (file-events "events-fixture"))]
(ifwait.run ["dummy0" "up"] #gen)
(match (pcall gen)
(true _) true
(false msg) (error "didn't detect dummy0 up event")))
(var upsies [])
(set fake-system
(fn [s]
(if (s:match "-u change addmember")
(table.insert upsies :u)
(s:match "-d change addmember")
(table.insert upsies :d))))
(fn newlink [name up running]
{:event "newlink"
:hwaddr "b6:7d:5c:38:89:1d"
:index (string.unpack ">i2" name)
:mtu 1500
: name
: running
:stamp 857161382
: up })
"when it gets events that don't match the interface, nothing happens"
(let [gen (-> [(newlink "eth1" "no" "no")] event-generator)]
(set upsies [])
(ifwait.run [ "-s" "addmember" "dummy0" "up"] #gen)
(expect= upsies []))
"when it gets an event that should start the service, the service starts"
(let [gen (->
[(newlink "dummy0" "no" "no")
(newlink "dummy0" "yes" "no")
(newlink "eth1" "no" "no")]
event-generator)]
(set upsies [])
(ifwait.run ["-s" "addmember" "dummy0" "up"] #gen)
(expect= upsies [:d :u]))
"when it gets an event that should stop the service, the service stops"
(let [gen (->
[(newlink "dummy0" "no" "no")
(newlink "dummy0" "yes" "no")
(newlink "dummy0" "no" "no")
]
event-generator)]
(set upsies [])
(ifwait.run ["-s" "addmember" "dummy0" "up"] #gen)
(expect= upsies [:d :u :d]))
"it does not call s6-rc again if the service is already in required state"
(let [gen (->
[(newlink "dummy0" "no" "no")
(newlink "dummy0" "yes" "no")
(newlink "dummy0" "yes" "yes")
(newlink "dummy0" "yes" "yes")
(newlink "dummy0" "yes" "no")
(newlink "dummy0" "no" "no")
]
event-generator)]
(set upsies [])
(ifwait.run ["-s" "addmember" "dummy0" "up"] #gen)
(expect= upsies [:d :u :d]))
"it handles an error return from s6-rc"
(set fake-system
(fn [s]
(if (s:match "-u change addmember")
(table.insert upsies :u)
(s:match "-d change addmember")
(table.insert upsies :d))
(error "false")
))
(let [gen (->
[(newlink "dummy0" "yes" "no")
(newlink "dummy0" "yes" "yes")
(newlink "dummy0" "yes" "yes")
(newlink "dummy0" "yes" "no")
(newlink "dummy0" "no" "no")
]
event-generator)]
(set upsies [])
(ifwait.run ["-s" "addmember" "dummy0" "up"] #gen)
(expect= upsies [:u :u :u :u]))
(print "OK")

View File

@ -15,10 +15,12 @@ let
in {
kernel
, commandLine
, commandLineDtbNode ? "bootargs"
, entryPoint
, extraName ? "" # e.g. socFamily
, loadAddress
, imageFormat
, alignment ? null
, dtb ? null
} : stdenv.mkDerivation {
name = "kernel.image";
@ -39,7 +41,7 @@ in {
'';
mungeDtbPhase = ''
dtc -I dtb -O dts -o tmp.dts ${dtb}
echo '/{ chosen { bootargs = ${builtins.toJSON commandLine}; }; };' >> tmp.dts
echo '/{ chosen { ${commandLineDtbNode} = ${builtins.toJSON commandLine}; }; };' >> tmp.dts
dtc -I dts -O dtb -o tmp.dtb tmp.dts
'';
@ -69,7 +71,7 @@ in {
};
};
_VARS
mkimage -f mkimage.its kernel.uimage
mkimage -f mkimage.its ${lib.optionalString (alignment != null) "-B 0x${lib.toHexString alignment}"} kernel.uimage
mkimage -l kernel.uimage
'';

View File

@ -4,7 +4,7 @@
, systemconfig
, execline
, lib
, services ? null
, config ? {}
, liminix
, pseudofile
, pkgs
@ -12,6 +12,7 @@
let
inherit (pseudofile) dir symlink;
inherit (liminix.services) oneshot;
paramConfig = config;
newRoot = "/run/maintenance";
sysconfig =
let
@ -25,8 +26,8 @@ let
emptyenv chroot . /bin/init
'';
base = {...} : {
config = {
services = services // {
config = {
services = {
banner = oneshot {
name = "banner";
up = "cat /etc/banner > /dev/console";
@ -60,6 +61,7 @@ let
../../modules/users.nix
../../modules/busybox.nix
base
({ ... } : paramConfig)
../../modules/s6
];
};

View File

@ -11,7 +11,7 @@ test -n "$contents" && for d in $contents; do
touch $out/${name}/contents.d/$d
done
for i in run notification-fd up down consumer-for producer-for pipeline-name ; do
for i in timeout-up timeout-down run notification-fd up down finish consumer-for producer-for pipeline-name restart-on-upgrade; do
test -n "$(printenv $i)" && (echo "$(printenv $i)" > $out/${name}/$i)
done

View File

@ -18,6 +18,7 @@ let
${commands}
'';
cleanupScript = name : ''
#!/bin/sh
if test -d ${prefix}/${name} ; then rm -rf ${prefix}/${name} ; fi
'';
service = {
@ -26,20 +27,25 @@ let
, run ? null
, up ? null
, down ? null
, finish ? null
, outputs ? []
, notification-fd ? null
, producer-for ? null
, consumer-for ? null
, pipeline-name ? null
, timeout-up ? 30000 # milliseconds
, timeout-down ? 0
, dependencies ? []
, contents ? []
, buildInputs ? []
, isTrigger ? false
} @ args:
stdenvNoCC.mkDerivation {
# we use stdenvNoCC to avoid generating derivations with names
# like foo.service-mips-linux-musl
inherit name serviceType up down run notification-fd
producer-for consumer-for pipeline-name;
inherit name serviceType up down run finish notification-fd
producer-for consumer-for pipeline-name timeout-up timeout-down;
restart-on-upgrade = isTrigger;
buildInputs = buildInputs ++ dependencies ++ contents;
dependencies = builtins.map (d: d.name) dependencies;
contents = builtins.map (d: d.name) contents;
@ -52,6 +58,7 @@ let
, outputs ? []
, notification-fd ? null
, dependencies ? []
, buildInputs ? []
, ...
} @ args:
let logger = service {
@ -63,9 +70,10 @@ let
pipeline-name = "${name}-pipeline";
};
in service (args // {
buildInputs = [ logger ];
buildInputs = buildInputs ++ [ logger ];
serviceType = "longrun";
run = serviceScript "${run}\n${cleanupScript name}";
run = serviceScript run;
finish = cleanupScript name;
producer-for = "${name}-log";
});

25
pkgs/lualinux/default.nix Normal file
View File

@ -0,0 +1,25 @@
{ lua, lib, fetchFromGitHub }:
let
pname = "lualinux";
src = fetchFromGitHub {
repo = "lualinux";
owner = "philanc";
rev = "1d4c962aad9cbe01c05df741b91e8b39c356362c";
hash = "sha256-+Ys4sERG+TI8nRzG38UP+KqbH0efspaX0j4IHCt56RI=";
};
in lua.pkgs.buildLuaPackage {
inherit pname;
version = "0.1"; # :shrug:
inherit src;
postPatch = ''
sed -i -e '/strip/d' Makefile
'';
makeFlags = [ "LUADIR=." "CC:=$(CC)" "STRIP=true" "lualinux.so" ];
installPhase = ''
mkdir -p "$out/lib/lua/${lua.luaversion}"
cp ${pname}.so "$out/lib/lua/${lua.luaversion}/"
'';
}

View File

@ -1,12 +1,19 @@
#!/usr/bin/env bash
ssh_command=${SSH_COMMAND-ssh}
if [ "$1" = "--no-reboot" ] ; then
reboot="true"
shift
else
reboot="reboot"
fi
reboot="reboot"
case "$1" in
"--no-reboot")
unset reboot
shift
;;
"--fast")
reboot="soft"
shift
;;
esac
target_host=$1
shift
@ -20,7 +27,16 @@ if toplevel=$(nix-build "$@" -A outputs.systemConfiguration --no-out-link); then
echo systemConfiguration $toplevel
min-copy-closure $target_host $toplevel
$ssh_command $target_host $toplevel/bin/install
$ssh_command $target_host "sync; source /etc/profile; reboot -f"
case "$reboot" in
reboot)
$ssh_command $target_host "sync; source /etc/profile; reboot"
;;
soft)
$ssh_command $target_host $toplevel/bin/restart-services
;;
*)
;;
esac
else
echo Rebuild failed
fi

22
pkgs/minisock/default.nix Normal file
View File

@ -0,0 +1,22 @@
{ lua, lib, fetchFromGitHub }:
let
pname = "minisock";
src = fetchFromGitHub {
repo = "minisock";
owner = "telent";
rev = "46e0470ff88c68f3a873dedbcf1dc351f4916b1a";
hash = "sha256-uTV5gpfEMvHMBgdu41Gy2uizc3K9bXtO5BiCY70cYUs=";
};
in lua.pkgs.buildLuaPackage {
inherit pname;
version = "0.1"; # :shrug:
inherit src;
makeFlags = [ "LUADIR=." "minisock.so" ];
installPhase = ''
mkdir -p "$out/lib/lua/${lua.luaversion}"
cp ${pname}.so "$out/lib/lua/${lua.luaversion}/"
'';
}

28
pkgs/nellie/default.nix Normal file
View File

@ -0,0 +1,28 @@
{ lua, lib, fetchpatch, fetchFromGitHub, stdenv }:
let pname = "nellie";
in lua.pkgs.buildLuaPackage {
inherit pname;
version = "0.1.1-1";
src = ./.;
buildPhase = "$CC -shared -l lua -o nellie.so nellie.c";
# for the checks to work you need to
# nix-build--option sandbox false
# otherwise the sandbox doesn't see any uevent messages
# doCheck = stdenv.hostPlatform == stdenv.buildPlatform;
checkPhase = ''
export LUA_CPATH=./?.so
lua test.lua
'';
installPhase = ''
mkdir -p "$out/lib/lua/${lua.luaversion}"
cp nellie.so "$out/lib/lua/${lua.luaversion}/"
'';
}

90
pkgs/nellie/nellie.c Normal file
View File

@ -0,0 +1,90 @@
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <linux/netlink.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int l_close_socket(lua_State *L) {
lua_getfield(L, 1, "fileno");
int fd = (int) lua_tointeger(L, -1);
close(fd);
return 0;
}
static int l_read_from_socket(lua_State *L) {
int length = 32;
if(lua_isnumber(L, 2))
length = lua_tointeger(L, 2);
lua_getfield(L, 1, "fileno");
int fd = (int) lua_tointeger(L, -1);
char *buf = (char *) malloc(length);
int bytes = recv(fd, buf, length, 0);
if(bytes > 0) {
lua_pushlstring(L, buf, bytes);
free(buf);
return 1;
} else {
free(buf);
return 0;
}
}
static int l_open_socket(lua_State *L) {
int netlink_fd = socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT);
struct sockaddr_nl sa;
memset(&sa, 0, sizeof(sa));
sa.nl_family = AF_NETLINK;
sa.nl_pid = getpid();
if(lua_isnumber(L, 1)) {
sa.nl_groups = lua_tointeger(L, 1);
lua_pop(L, 1);
}
else {
sa.nl_groups = 4; /* group 4 is rebroadcasts from mdevd */
}
if(bind(netlink_fd, (struct sockaddr *) &sa, sizeof(sa))==0) {
lua_newtable(L);
lua_pushliteral(L, "fileno");
lua_pushinteger(L, netlink_fd);
lua_settable(L, 1);
lua_pushliteral(L, "read");
lua_pushcfunction(L, l_read_from_socket);
lua_settable(L, 1);
lua_pushliteral(L, "close");
lua_pushcfunction(L, l_close_socket);
lua_settable(L, 1);
return 1;
} else {
return 0;
}
}
static const struct luaL_Reg funcs [] = {
{"open", l_open_socket},
{NULL, NULL} /* sentinel */
};
/* "luaopen_" prefix is magic and tells lua to run this function
* when it dlopens the library
*/
int luaopen_nellie (lua_State *L) {
luaL_newlib(L, funcs);
return 1;
}

6
pkgs/nellie/test.lua Normal file
View File

@ -0,0 +1,6 @@
local nellie = require('nellie')
print('dfg')
local f = nellie.open(2)
print(string.byte(f:read(1000), 0, 60))
print("CLOSED", f:close())

View File

@ -2,5 +2,6 @@
writeFennelScript
, anoia
, lua
, lualinux
}:
writeFennelScript "odhcpc-script" [anoia lua.pkgs.luafilesystem] ./odhcp6-script.fnl
writeFennelScript "odhcpc-script" [anoia lualinux] ./odhcp6-script.fnl

View File

@ -1,6 +1,7 @@
{
fetchFromGitHub
, writeShellScript
, pkgsBuildBuild
}:
let
src = fetchFromGitHub {
@ -14,6 +15,7 @@ let
cp -av ${src}/target/linux/generic/files/* .
chmod -R u+w .
cp -av ${src}/target/linux/${family}/files/* .
chmod -R u+w .
test -d ${src}/target/linux/${family}/files-5.15/ && cp -av ${src}/target/linux/${family}/files-5.15/* .
chmod -R u+w .
patches() {
@ -35,4 +37,25 @@ in {
applyPatches.ramips = doPatch "ramips";
applyPatches.mediatek = doPatch "mediatek"; # aarch64
applyPatches.mvebu = doPatch "mvebu"; # arm
applyPatches.rt2x00 = ''
PATH=${pkgsBuildBuild.patchutils}/bin:$PATH
for i in ${src}/package/kernel/mac80211/patches/rt2x00/6*.patch ; do
fixed=$(basename $i).fixed
sed '/depends on m/d' < $i | sed 's/CPTCFG_/CONFIG_/g' | recountdiff | filterdiff -x '*/local-symbols' > $fixed
case $fixed in
606-*)
;;
611-*)
filterdiff -x '*/rt2x00.h' < $fixed | patch --forward -p1
;;
601-*|607-*)
filterdiff -x '*/rt2x00_platform.h' < $fixed | patch --forward -p1
;;
*)
cat $fixed | patch --forward -p1
;;
esac
done
'';
}

View File

@ -21,11 +21,27 @@ in stdenvNoCC.mkDerivation {
if test -d $i; then
for j in $i/* ; do
if test -f $j/type ; then
if test -e $j/restart-on-upgrade; then
flag=force-restart
else
unset flag
fi
case $(cat $j/type) in
longrun|oneshot)
# s6-rc-update only wants atomics in its
# restarts file
echo $(basename $j) " " ''${flag-$i} >> $out/hashes
;;
*)
;;
esac
srcs="$srcs $i"
fi
done
fi
done
s6-rc-compile $out/compiled $srcs
'';
}
s6-rc-db -c $out/compiled contents default
mv $out/hashes $out/compiled
'';
}

View File

@ -6,6 +6,7 @@
{
writeText
, writeFennelScript
, lib
, s6-init-bin
, closureInfo
@ -52,7 +53,7 @@ let
chown = if uid>0 || gid>0
then "\nCHOWN(${qpathname},${toString uid},${toString gid});\n"
else "";
in "${cmd} ${chown}";
in "unlink(${qpathname}); ${cmd} ${chown}";
in mapAttrsToList (makeFile prefix) attrset;
activateScript = attrset: writeText "makedevs.c" ''
#include "defs.h"
@ -80,6 +81,7 @@ in attrset:
cp $closure/store-paths $out/etc/nix-store-paths
$STRIP --remove-section=.note --remove-section=.comment --strip-all makedevs -o $out/bin/activate
ln -s ${s6-init-bin}/bin/init $out/bin/init
cp -p ${writeFennelScript "restart-services" [] ./restart-services.fnl} $out/bin/restart-services
cat > $out/bin/install <<EOF
#!/bin/sh -e
prefix=\''${1-/}

View File

@ -0,0 +1,40 @@
(fn hashes-from-file [name]
(with-open [f (assert (io.open name :r) name)]
(accumulate [h []
l #(f:read "*l")]
(let [(name hash) (string.match l "([^%s]+) +([^%s]+)")]
(if name
(doto h (tset name hash))
h)))))
(fn write-restarts [old new]
(let [old-hashes (hashes-from-file old)
new-hashes (hashes-from-file new)]
(with-open [f (io.open "/tmp/restarts" :w)]
(each [n h (pairs old-hashes)]
(let [new (. new-hashes n)]
(when (or (= h "force-restart")
(= new "force-restart")
(not (= h new)))
(f:write (.. n " restart\n"))))))))
(fn exec [text command]
(io.write (.. text ": "))
(match (os.execute command)
res (print "[OK]")
(nil err) (error (.. "[FAILED " err "]"))))
(let [mypath (: (. arg 0) :match "(.*/)")
activate (.. mypath "activate /")
old-compiled "/run/s6-rc/compiled/"
new-compiled "/etc/s6-rc/compiled/"]
(exec "installing FHS files" activate)
(write-restarts (.. old-compiled "hashes") (.. new-compiled "hashes"))
(exec "updating service database"
(.. "s6-rc-update -f /tmp/restarts " new-compiled))
(exec "starting services" (.. "s6-rc -u -p change default"))
)

View File

@ -0,0 +1,12 @@
loopback /nix/store/3k7yp08465k8z8a1514slansj0hbrvbq-loopback
default /nix/store/5247rr0mcr17x9lkaqjiywxmn4q3ibgd-default
lan.link.a.10.3.0.1 /nix/store/99ic5bryzbx12ip4v8sh9cdrv15c8qwl-lan.link.a.10.3.0.1
lo.link.a.-1 /nix/store/a2yvj42ghcg8iyvi51zcmzmpwwkrx30l-lo.link.a.-1
sshd /nix/store/afgznmavi6s9dxkv4vnn9xwd7qz5kk35-sshd
lo.link /nix/store/dz0nvlf16lqv9yndas0sk0xqh7jvx8gm-lo.link
lo.link.a.127.0.0.1 /nix/store/jl018kbgxcnyb70s0cfw19q1zqa0pval-lo.link.a.127.0.0.1
lan.link /nix/store/lxvmhb7ax46s2akj9lsivcq494kmx0hm-lan.link
lan.link.a.10.3.0.1.dnsmasq-log /nix/store/rk8265ikd9lj3fpibyrw0kal32h0ksqg-lan.link.a.10.3.0.1.dnsmasq-log
hostname /nix/store/xfy0ymvnd0zv6wcd26h405f85hfkd8wq-hostname
sshd-log /nix/store/xj58nxvkacsjg3rvwb4bxcqnhaav4d3s-sshd-log
lan.link.a.10.3.0.1.dnsmasq /nix/store/z4l7h9pr4an9aq921cs9wrlvf9b8xr48-lan.link.a.10.3.0.1.dnsmasq

View File

@ -0,0 +1,12 @@
loopback /nix/store/3k7yp08465k8z8a1514slansj0hbrvbq-loopback
default /nix/store/5247rr0mcr17x9lkaqjiywxmn4q3ibgd-default
lan.link.a.10.3.0.1 /nix/store/99ic5bryzbx12ip4v8sh9cdrv15c8qwl-lan.link.a.10.3.0.1
lo.link.a.-1 /nix/store/a2yvj42ghcg8iyvi51zcmzmpwwkrx30l-lo.link.a.-1
sshd /nix/store/afgznmavi6s9dxkv4vnn9xwd7qz5kk35-sshd
lo.link /nax/store/dz0nvlf16lqv9yndas0sk0xqh7jvx8gm-lo.link
lo.link.a.127.0.0.1 /nix/store/jl018kbgxcnyb70s0cfw19q1zqa0pval-lo.link.a.127.0.0.1
lan.link /nix/store/lxvmhb7ax46s2akj9lsivcq494kmx0hm-lan.link
lan.link.a.10.3.0.1.dnsmasq-log /nix/store/rk8265ikd9lj3fpibyrw0kal32h0ksqg-lan.link.a.10.3.0.1.dnsmasq-log
hostname /nax/store/xfy0ymvnd0zv6wcd26h405f85hfkd8wq-hostname
sshd-log /nax/store/xj58nxvkacsjg3rvwb4bxcqnhaav4d3s-sshd-log

View File

@ -0,0 +1,27 @@
{
lua
, nellie
, lualinux
, writeFennel
, runCommand
, anoia
, fennel
, stdenv
, fennelrepl
}:
stdenv.mkDerivation {
name = "uevent-watch";
src = ./.;
nativeBuildInputs = [ fennelrepl ];
installPhase = ''
mkdir -p $out/bin
cp -p ${writeFennel "uevent-watch" {
packages = [fennel anoia nellie lualinux];
mainFunction = "run";
} ./watch.fnl} $out/bin/uevent-watch
'';
checkPhase = ''
fennelrepl ./test.fnl
'';
doCheck = true;
}

View File

@ -0,0 +1,25 @@
add@/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1
SUBSYSTEM=block
DEVNAME=/dev/sdb1
DEVTYPE=partition
DISKSEQ=33
PARTN=1
SEQNUM=82381
MAJOR=8
PARTNAME=backup-disk
MINOR=17
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1
ACTION=remove
DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0/host1/target1:0:0/1:0:0:0/block/sdb/sdb1
SUBSYSTEM=block
DEVNAME=/dev/sdb1
DEVTYPE=partition
DISKSEQ=33
PARTN=1
SEQNUM=82386
PARTNAME=backup-disk
MAJOR=8
MINOR=17

View File

@ -0,0 +1,33 @@
(local watch (require :watch))
;; Events come from the devout socket as an initial summary line
;; followed by a NUL character followed by newline-separated key=value
;; pairs. For ease of editing we don't have NULs in events.txt,
;; so we need to massage it into shape here
(local events
(with-open [f (io.open "./events.txt" :r)]
(let [text (string.gsub (f:read "*a") "\n\n" "\0") ]
(icollect [n (string.gmatch text "([^\0]+)")]
(string.gsub n "\n" "\0" 1)))))
(fn next-event []
(var i 0)
(fn []
(let [i_ (+ 1 i)
e (. events i_)]
(set i i_)
e)))
;; this tests event parsing but not whether anything
;; happens as a result of processing them
(watch.run-with-fh
{
:read (next-event)
:write (fn [payload] )
}
["-s" "foo" "-n" (os.getenv "TMPDIR") "partname=backup-disk" ]
)
(print "OK")

View File

@ -0,0 +1,77 @@
(local { : assoc : system : dirname } (require :anoia))
(local { : mktree : rmtree : symlink } (require :anoia.fs))
(local { : AF_LOCAL : SOCK_STREAM } (require :anoia.net.constants))
(local ll (require :lualinux))
(local { : view } (require :fennel))
(fn parse-args [args]
(match args
["-s" service & rest] (assoc (parse-args rest) :service service)
["-n" path & rest] (assoc (parse-args rest) :linkname path)
matches { :matches (table.concat matches " ") }
_ nil))
(fn %% [fmt ...] (string.format fmt ...))
(var up :unknown)
(fn start-service [devname linkname service]
(match (symlink (.. "/dev/" devname ) linkname)
ok (pcall system (%% "s6-rc -b -u change %q" service))
(nil err) false))
(fn stop-service [linkname service]
(match (pcall system (%% "s6-rc -b -d change %q" linkname service))
ok (os.remove linkname)
(nil err) false))
(fn toggle-service [devname linkname service wanted?]
(when (not (= up wanted?))
(set up
(if wanted?
(start-service devname linkname service)
(not (stop-service linkname service))))))
(fn parse-uevent [s]
(when s
(let [(nl nxt) (string.find s "\0" 1 true)]
(collect [k v (string.gmatch
(string.sub s (+ 1 nxt))
"(%g-)=(%g+)")]
(k:lower) v))))
(fn run-with-fh [fh args]
(set up :unknown)
(let [parameters
(assert (parse-args args) (.. "can't parse args: " (table.concat args " ")))]
(mktree (dirname parameters.linkname))
(var finished? false)
(print "registering for events" (fh:write parameters.matches))
(while (not finished?)
(let [e (parse-uevent (fh:read))]
(when e
(let [wanted? (. {:add true :change true} e.action)]
(toggle-service e.devname parameters.linkname parameters.service wanted?)))
(set finished? (= e nil))
))))
(fn unix-connect [pathname]
(let [addr (string.pack "=Hz" AF_LOCAL pathname)]
(match (ll.socket AF_LOCAL SOCK_STREAM 0)
sock (doto sock (ll.connect addr))
(nil err) (error err))))
(fn run [args]
(let [fd (assert (unix-connect "/run/devout.sock"))
devout {
:read #(ll.read fd)
:write #(ll.write fd $2)
:close #(ll.close fd)
}]
(run-with-fh devout arg)))
{ : run : run-with-fh }

View File

@ -0,0 +1,16 @@
{
stdenv
, openwrt
}:
stdenv.mkDerivation {
name = "zyxel-bootconfig";
inherit (openwrt) src;
sourceRoot = "openwrt-source/package/utils/zyxel-bootconfig/src";
installPhase = ''
mkdir -p $out/bin
install -Dm544 zyxel-bootconfig $out/bin/zyxel-bootconfig
'';
meta = {
mainProgram = "zyxel-bootconfig";
};
}

View File

@ -8,4 +8,6 @@
min-copy-closure = import ./min-copy-closure/test.nix;
fennel = import ./fennel/test.nix;
tftpboot = import ./tftpboot/test.nix;
updown = import ./updown/test.nix;
inout = import ./inout/test.nix;
}

View File

@ -0,0 +1,55 @@
{ config, pkgs, lib, modulesPath, ... } :
let
inherit (pkgs.liminix.services) bundle oneshot longrun;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) serviceFns;
svc = config.system.service;
in rec {
imports = [
"${modulesPath}/dhcp6c"
"${modulesPath}/dnsmasq"
"${modulesPath}/firewall"
"${modulesPath}/hostapd"
"${modulesPath}/network"
"${modulesPath}/ssh"
"${modulesPath}/mount"
"${modulesPath}/mdevd.nix"
];
filesystem = dir { srv = dir {}; };
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";
MSDOS_PARTITION = "y";
EFI_PARTITION = "y";
EXT4_FS = "y";
EXT4_USE_FOR_EXT2 = "y";
FS_ENCRYPTION = "y";
};
};
rootfsType = "jffs2";
hostname = "inout";
services.mount_backup_disk = svc.mount.build {
partlabel = "backup-disk";
mountpoint = "/srv";
fstype = "ext4";
};
}

55
tests/inout/script.expect Normal file
View File

@ -0,0 +1,55 @@
set timeout 10
set when [lindex $argv 0];
spawn socat -,echo=0,icanon=1 unix-connect:vm/monitor
set monitor_id $spawn_id
spawn socat unix-connect:vm/console -
set console_id $spawn_id
proc chat {instr outstr} {
expect {
$instr { send $outstr }
timeout { exit 1 }
}
}
proc adddevice { } {
global monitor_id console_id spawn_id
set spawn_id $monitor_id
chat "QEMU" "device_add usb-storage,bus=xhci.0,drive=usbstick\n"
chat "(qemu)" "version\r"
set spawn_id $console_id
expect {
"sda: sda1" { }
timeout { exit 1 }
}
}
if { $when eq "early" } {
adddevice
}
expect "BusyBox"
chat "#" "PS1=RE\\ADY_\\ ; stty -echo \r"
chat "READY_" "tail -f /run/uncaught-logs/current & \rs6-rc -b -a list\r"
chat "mount" "\r"
if { $when eq "late" } {
adddevice
}
send "\r"
set timeout 20
chat "READY_" "sleep 3; grep /srv /proc/mounts && hostname\r"
expect {
"inout" { }
timeout { exit 1 }
}

37
tests/inout/test.nix Normal file
View File

@ -0,0 +1,37 @@
{
liminix
, nixpkgs
}:
let img = (import liminix {
device = import "${liminix}/devices/qemu/";
liminix-config = ./configuration.nix;
}).outputs.vmroot;
pkgs = import <nixpkgs> { overlays = [(import ../../overlay.nix)]; };
in pkgs.runCommand "check" {
nativeBuildInputs = with pkgs; [
expect
socat
e2fsprogs
util-linux # for sfdisk, fallocate
parted
] ;
} ''
mkdir vm
dd if=/dev/zero of=./vm/stick.e2fs bs=1M count=32
mkfs.ext2 -L backup-disk ./vm/stick.e2fs
dd if=/dev/zero of=./vm/stick.img bs=1M count=38
dd if=./vm/stick.e2fs of=./vm/stick.img bs=512 seek=34 conv=notrunc
parted -s ./vm/stick.img -- mklabel gpt mkpart backup-disk ext2 34s -0M
{
${img}/run.sh --background ./vm --flag -device --flag usb-ehci,id=xhci --flag -drive --flag if=none,id=usbstick,format=raw,file=$(pwd)/vm/stick.img
expect ${./script.expect} late
kill $(cat ./vm/pid)
${img}/run.sh --background ./vm --flag -device --flag usb-ehci,id=xhci --flag -drive --flag if=none,id=usbstick,format=raw,file=$(pwd)/vm/stick.img
expect ${./script.expect} early
} | tee $out
''

View File

@ -32,9 +32,5 @@ in {
};
rootfsType = "jffs2";
services.default = lib.mkForce (target {
name = "default";
contents = with config.services; [ loopback ntp defaultroute4 sshd dhcpv4 ];
});
};
}

29
tests/min-copy-closure/test-liminix-rebuild.sh Normal file → Executable file
View File

@ -1,14 +1,26 @@
#!/usr/bin/env nix-shell
#! nix-shell -v -i bash -p expect socat
# This is a test for liminix-rebuild. It's not a CI test because
# liminix-rebuild calls nix-build so won't run inside a derivation,
# meaning you have to remember to run it manually when changing
# liminix-rebuild
# nix-shell -p expect socat --run "sh ./tests/min-copy-closure/test-liminix-rebuild.sh "
. tests/test-helpers.sh
set -e
while test -n "$1"; do
case $1 in
--fast)
FAST=true
;;
*)
;;
esac
shift
done
here=$(pwd)/tests/min-copy-closure
top=$(pwd)
@ -31,7 +43,7 @@ mkdir ./vm
cat ${rootfs} > rootfs
truncate -s 24M rootfs
truncate -s 32M rootfs
resize2fs rootfs
dd if=rootfs of=disk-image bs=512 seek=4 conv=sync
@ -50,9 +62,12 @@ echo "READY"
touch known_hosts
export SSH_COMMAND="ssh -o UserKnownHostsFile=${work}/known_hosts -o StrictHostKeyChecking=no -p 2022 -i ${here}/id"
(cd ${top} && liminix-rebuild root@localhost -I liminix-config=${here}/with-figlet.nix --arg device "import ./devices/qemu-armv7l")
if test -n "$FAST"; then
(cd ${top} && liminix-rebuild --fast root@localhost -I liminix-config=${here}/with-figlet.nix --argstr deviceName qemu-armv7l)
cd ${work} && expect $here/wait-for-soft-restart.expect
else
(cd ${top} && liminix-rebuild root@localhost -I liminix-config=${here}/with-figlet.nix --arg device "import ./devices/qemu-armv7l")
cd ${work} && expect $here/wait-for-reboot.expect
fi
ls -l vm
cd ${work} && expect $here/wait-for-reboot.expect
cd / ; rm -rf $work

View File

@ -0,0 +1,13 @@
proc chat {instr outstr} {
expect {
$instr { send $outstr }
timeout { exit 1 }
}
}
spawn socat unix-connect:vm/console -
send "exit\r"
chat "BusyBox" "\r"
chat "#" "stty -echo; type -p figlet\r"
chat "figlet-armv7l-unknown-linux" "s6-rc -a list |grep w\\inkle\r"
chat "winkle" "poweroff\r"

View File

@ -5,4 +5,9 @@ send "\r\n"
expect {
"# " { send "hostname\r\n" };
}
expect "(none)"
expect {
"(none)" {}
"liminix" {}
timeout { exit(1) }
}

View File

@ -4,4 +4,11 @@
defaultProfile.packages = with pkgs; [
figlet
];
services.ripvanwinkle = pkgs.liminix.services.longrun {
name = "winkle";
run = ''
echo SLEEPING > /dev/console
sleep 3600
'';
};
}

View File

@ -0,0 +1,55 @@
{ 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 {
imports = [
../../modules/bridge
../../modules/dhcp6c
../../modules/dnsmasq
../../modules/firewall
../../modules/hostapd
../../modules/network
../../modules/ssh
../../modules/vlan
../../modules/wlan.nix
];
rootfsType = "jffs2";
hostname = "updown";
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;
[ lan ];
};
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";
# ];
# };
defaultProfile.packages = with pkgs; [
min-collect-garbage
# strace
# ethtool
tcpdump
];
}

View File

@ -0,0 +1,64 @@
set timeout 10
spawn socat unix-connect:vm/monitor -
set monitor_id $spawn_id
expect "(qemu)"
send "set_link virtio-net-pci.1 off\n"
expect "(qemu)"
send "set_link virtio-net-pci.0 off\n"
expect "(qemu)"
send "c\r\n"
spawn socat unix-connect:vm/console -
set console_id $spawn_id
expect "BusyBox"
expect "#" { send "PS1=RE\\ADY_\\ ; stty -echo \r" }
expect "READY_" { send "s6-rc -b -a list\r" } ; # -b waits for s6-rc lock
expect "READY_" { send "ls /sys/class/net/lan/master\r" }
expect {
"No such file or directory" { }
timeout { exit 1 }
}
expect "READY_" { send "cat /sys/class/net/lan/operstate\r" }
expect {
"down" { }
"up" { exit 1 }
}
expect "READY_" { send "s6-rc -a -u change\r" }
expect {
"unable to take locks" { exit 1 }
"READY_" { send "\r" }
}
set spawn_id $monitor_id
send "\r"
expect "(qemu)"
send "set_link virtio-net-pci.1 on\n"
expect "(qemu)"
send "set_link virtio-net-pci.0 on\n"
expect "(qemu)"
set spawn_id $console_id
expect "entered forwarding state"
send "\r"
expect "READY_" { send "cat /sys/class/net/lan/operstate\r" }
expect {
"down" { exit 1 }
"up" { }
}
expect "READY_" { send "cat /sys/class/net/lan/master/uevent\r" }
expect {
"INTERFACE=int" { }
timeout { exit 1 }
}
expect "READY_" { send "s6-rc listall int.link.a.10.8.0.1.member.lan.link ; hostname\r" }
expect {
"updown" {}
timeout { exit 1 }
}

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