development.adoc (23293B)
1 = For Developers 2 3 In any Nix-based system the line between "configuration" 4 and "development" is less of a line and more of a continuum. 5 This section covers some topics further towards the latter end. 6 7 == Writing modules and services 8 9 It helps here to know NixOS! Liminix uses the NixOS module 10 infrastructure code, meaning that everything that has been written for 11 NixOS about the syntax, the type system, and the rules for combining 12 configuration values from different sources is just as applicable 13 here. 14 15 === Services 16 17 For the most part, for common use cases, we hope that Liminix modules 18 provide service templates for all the services you will need, and you 19 will only have to pass the right parameters to `+build+`. 20 21 But if you're reading this then our hopes are in vain. To create a 22 custom service of your own devising, use the [.title-ref]#oneshot# or 23 [.title-ref]#longrun# functions: 24 25 * a "longrun" service is the "normal" service concept: it has a `+run+` 26 action which describes the process to start, and it watches that process 27 to restart it if it exits. The process should not attempt to daemonize 28 or "background" itself, otherwise s6-rc will think it died. Whatever it 29 prints to standard output/standard error will be logged. 30 31 [source,nix] 32 ---- 33 config.services.cowsayd = pkgs.liminix.services.longrun { 34 name = "cowsayd"; 35 run = "${pkgs.cowsayd}/bin/cowsayd --port 3001 --breed hereford"; 36 # don't start this until the lan interface is ready 37 dependencies = [ config.services.lan ]; 38 } 39 ---- 40 41 * a "oneshot" service doesn't have a process attached. It consists of 42 `+up+` and `+down+` actions which are bits of shell script that are run 43 at the appropriate points in the service lifecycle 44 45 [source,nix] 46 ---- 47 config.services.greenled = pkgs.liminix.services.oneshot { 48 name = "greenled"; 49 up = '' 50 echo 17 > /sys/class/gpio/export 51 echo out > /sys/class/gpio/gpio17/direction 52 echo 0 > /sys/class/gpio/gpio17/value 53 ''; 54 down = '' 55 echo 0 > /sys/class/gpio/gpio17/value 56 ''; 57 } 58 ---- 59 60 Services may have dependencies: as you see above in the `+cowsayd+` 61 example, it depends on some service called `+config.services.lan+`, 62 meaning that it won't be started until that other service is up. 63 64 ===== Service outputs 65 66 Outputs are a mechanism by which a service can provide data which may be 67 required by other services. For example: 68 69 * the DHCP client service can expect to receive nameserver address 70 information as one of the fields in the response from the DHCP server: 71 we provide that as an output which a dependent service for a stub name 72 resolver can use to configure its upstream servers. 73 * a service that creates a new network interface (e.g. ppp) will provide 74 the name of the interface (`+ppp0+`, or `+ppp1+` or `+ppp7+`) as an 75 output so that a dependent service can reference it to set up a route, 76 or to configure firewall rules. 77 78 A service `+myservice+` should write its outputs as files in 79 `+/run/services/outputs/myservice+`: you can look around this directory 80 on a running Liminix system to see how it's used currently. Usually we 81 use the `+in_outputs+` shell function in the `+up+` or `+run+` 82 attributes of the service: 83 84 [source,shell] 85 ---- 86 (in_outputs ${name} 87 for i in lease mask ip router siaddr dns serverid subnet opt53 interface ; do 88 (printenv $i || true) > $i 89 done) 90 ---- 91 92 The outputs are just files, so technically you can read them using 93 anything that can read a file. Liminix has two "preferred" mechanisms, 94 though: 95 96 ===== One-off lookups 97 98 In any context that ends up being evaluated by the shell, use `+output+` 99 to print the value of an output 100 101 [source,nix] 102 ---- 103 services.defaultroute4 = svc.network.route.build { 104 via = "$(output ${services.wan} address)"; 105 target = "default"; 106 dependencies = [ services.wan ]; 107 }; 108 ---- 109 110 ===== Continuous updates 111 112 The downside of using shell functions in downstream service startup 113 scripts is that they only run when the service starts up: if a service 114 output _changes_, the downstream service would have to be restarted to 115 notice the change. Sometimes this is OK but other times the downstream 116 has no other need to restart, if it can only get its new data. 117 118 For this case, there is the `+anoia.svc+` Fennel library, which allows 119 you to write a simple loop which is iterated over whenever a service's 120 outputs change. This code is from 121 `+modules/dhcp6c/acquire-wan-address.fnl+` 122 123 [source,fennel] 124 ---- 125 (fn update-addresses [wan-device addresses new-addresses exec] 126 ;; run some appropriate "ip address [add|remove]" commands 127 ) 128 129 (fn run [] 130 (let [[state-directory wan-device] arg 131 dir (svc.open state-directory)] 132 (accumulate [addresses [] 133 v (dir:events)] 134 (update-addresses wan-device addresses 135 (or (v:output "address") []) system)))) 136 ---- 137 138 The `+output+` method seen here accepts a filename (relative to the 139 service's output directory), or a directory name. It returns the first 140 line of that file, or for directories it returns a table (Lua's 141 key/value datastructure, similar to a hash/dictionary) of the outputs in 142 that directory. 143 144 ===== Design considerations for outputs 145 146 For preference, outputs should be short and simple, and not require 147 downstream services to do complicated parsing in order to use them. 148 Shell commands in Liminix are run using the Busybox shell which doesn't 149 have the niceties evel of an advanced shell like Bash, let alone those of a 150 real programming language. 151 152 Note also that the Lua `+svc+` library only reads the first line of each 153 output. 154 155 156 === Modules 157 158 Modules in Liminix conventionally live in 159 `+modules/somename/default.nix+`. If you want or need to write your own, 160 you may wish to refer to the examples there in conjunction with reading 161 this section. 162 163 A module is a function that accepts `+{lib, pkgs, config, ... }+` and 164 returns an attrset with keys `+imports, options, config+`. 165 166 * `+imports+` is a list of paths to the other modules required by this 167 one 168 * `+options+` is a nested set of option declarations 169 * `+config+` is a nested set of option definitions 170 171 The NixOS manual section 172 https://nixos.org/manual/nixos/stable/#sec-writing-modules[Writing NixOS 173 Modules] is a quite comprehensive reference to writing NixOS modules, 174 which is also mostly applicable to Liminix except that it doesn't cover 175 service templates. 176 177 178 ==== Service templates 179 180 Although you can define services "ad hoc" using `longrun` or `oneshot` 181 <<_writing_services,as above>>, this approach has limitations if 182 you're writing code intended for wider use. Services in the 183 modules bundled with Liminix are implemented following a pattern we 184 call "service templates": functions that accept a _type-checked_ 185 attrset and return an appropriately configured service that can be 186 assigned by the caller to a key in ``config.services``. 187 188 To expose a service template in a module, it needs the following: 189 190 * an option declaration for `+system.service.myservicename+` with the 191 type of `+liminix.lib.types.serviceDefn+` 192 193 [source,nix] 194 ---- 195 options = { 196 system.service.cowsay = mkOption { 197 type = liminix.lib.types.serviceDefn; 198 }; 199 }; 200 ---- 201 202 * an option definition for the same key, which specifies where to import 203 the service template from (often `+./service.nix+`) and the types of its 204 parameters. 205 206 [source,nix] 207 ---- 208 config.system.service.cowsay = config.system.callService ./service.nix { 209 address = mkOption { 210 type = types.str; 211 default = "0.0.0.0"; 212 description = "Listen on specified address"; 213 example = "127.0.0.1"; 214 }; 215 port = mkOption { 216 type = types.port; 217 default = 22; 218 description = "Listen on specified TCP port"; 219 }; 220 breed = mkOption { 221 type = types.str; 222 default = "British Friesian" 223 description = "Breed of the cow"; 224 }; 225 }; 226 ---- 227 228 Then you need to provide the service template itself, probably in 229 `+./service.nix+`: 230 231 [source,nix] 232 ---- 233 { 234 # any nixpkgs package can be named here 235 liminix 236 , cowsayd 237 , serviceFns 238 , lib 239 }: 240 # these are the parameters declared in the callService invocation 241 { address, port, breed} : 242 let 243 inherit (liminix.services) longrun; 244 inherit (lib.strings) escapeShellArg; 245 in longrun { 246 name = "cowsayd"; 247 run = "${cowsayd}/bin/cowsayd --address ${address} --port ${builtins.toString port} --breed ${escapeShellArg breed}"; 248 } 249 ---- 250 251 TIP: Not relevant to module-based services specifically, but a common gotcha 252 when specifiying services is forgetting to transform "rich" parameter 253 values into text when composing a command for the shell to execute. Note 254 here that the port number, an integer, is stringified with `+toString+`, 255 and the name of the breed, which may contain spaces, is escaped with 256 `+escapeShellArg+` 257 258 === Types 259 260 All of the NixOS module types are available in Liminix. These 261 Liminix-specific types also exist in `+pkgs.liminix.lib.types+`: 262 263 * `+service+`: an s6-rc service 264 * `+interface+`: an s6-rc service which specifies a network interface 265 * `+serviceDefn+`: a service "template" definition 266 267 In the future it is likely that we will extend this to include other 268 useful types in the networking domain: for example; IP address, network 269 prefix or netmask, protocol family and others as we find them. 270 271 272 === Emulated devices 273 274 Unless your changes depend on particular hardware devices, you may 275 want to test your new/changed module with one of the emulated 276 "devices" which runn on your build machine using the free 277 http://www.qemu.org[QEMU machine emulator]. They are 278 279 * `qemu`(MIPS) 280 * `qemu-armv7l`(32 bit ARM) 281 * `qemu-aarch64` (64 bit ARM) 282 283 This means you don't need to keep flashing or messing with U-Boot: it 284 also enables testing against emulated network peers using 285 https://wiki.qemu.org/Documentation/Networking#Socket[QEMU socket 286 networking], which may be preferable to letting Liminix loose on your 287 actual LAN. To build, 288 289 [source,console] 290 ---- 291 nix-build -I liminix-config=path/to/your/configuration.nix --arg device "import ./devices/qemu" -A outputs.default 292 ---- 293 294 This creates a `+result/+` directory containing a `+vmlinux+` and a 295 `+rootfs+`, and a shell script `+run.sh+` which invokes QEMU to run 296 that kernel with that filesystem. It connects the Liminix serial console 297 and the https://www.qemu.org/docs/master/system/monitor.html[QEMU 298 monitor] to stdin/stdout. Use `^P` (not `^A`) to switch to the monitor. 299 300 // FIXME should add a `connect.sh` script instead of requiring nix-shell here 301 302 If you run with `+--background /path/to/some/directory+` as the first 303 parameter, it will fork into the background and open Unix sockets in 304 that directory for console and monitor. Use `+nix-shell --run 305 connect-vm+` to connect to either of these sockets, and ^O to 306 disconnect. 307 308 [[qemu-networking]] 309 ===== Networking 310 311 VMs can network with each other using QEMU socket networking. We observe 312 these conventions, so that we can run multiple emulated instances and 313 have them wired up to each other in the right way: 314 315 * multicast 230.0.0.1:1234 : access (interconnect between router and 316 "isp") 317 * multicast 230.0.0.1:1235 : lan 318 * multicast 230.0.0.1:1236 : world (the internet) 319 320 Any VM started by a `+run.sh+` script is connected to "lan" and 321 "access". The emulated upstream (see below) runs PPPoE and is 322 connected to "access" and "world". 323 324 ===== Upstream connection 325 326 In pkgs/routeros there is a derivation to install and configure 327 https://mikrotik.com/software[Mikrotik RouterOS] as a PPPoE access 328 concentrator connected to the `+access+` and `+world+` networks, so that 329 Liminix PPPoE client support can be tested without actual hardware. 330 331 This is made available as the `+routeros+` command in `+buildEnv+`, so 332 you can do something like: 333 334 .... 335 mkdir ros-sockets 336 nix-shell 337 nix-shell$ routeros ros-sockets 338 nix-shell$ connect-vm ./ros-sockets/console 339 .... 340 341 to start it and connect to it. Note that by default it runs in the 342 background. It is connected to "access" and "world" virtual networks and 343 runs a PPPoE service on "access" - so a Liminix VM with a PPPOE client 344 can connect to it and thus reach the virtual internet. [ check, but 345 pretty sure this is not the actual internet ] 346 347 [.title-ref]#Liminix does not provide RouterOS licences and it is your 348 own responsibility if you use this to ensure you're compliant with the 349 terms of Mikrotik's licencing. It may be supplemented or replaced in 350 time with configurations for RP-PPPoE and/or Accel PPP.# 351 352 == Hardware hacking/porting to new device 353 354 The steps to port to a new hardware device are largely undocumented at 355 present (although this hasn't stopped people from figuring it out 356 already). As an outline I would recommend 357 358 * choose hardware that OpenWrt already supports, otherwise you will 359 probably spend a lot of time writing kernel code. The OpenWrt kernel 360 supports many network interfaces and other hardware for a lot of 361 hardware boards that might only just about be able to boot Linux on a 362 serial port if you stick to mainline Linux 363 364 * work out how to get a serial console on it. You are unlikely to get 365 working networking on your first go at boulding a kernel 366 367 * find the most similar device in Liminiux and copy 368 `devices/existing-similar-device` to `devices/cool-new-device` as a 369 starting point 370 371 * use the kernel configuration (`/proc/config.gz`) from OpenWrt as a 372 reference for the kernel config you'll need to specify 373 in `devices/cool-new-device/default.nix` 374 375 * break it down into achieveable goals. Your first goal should be 376 something that can TFTP boot the kernel as far as a running 377 userland. Networking is harder, Wifi often much harder - it 378 sometimes also depends on having working flash _even if_ you're TFTP 379 booting because the driver expects to load wifi firmware or 380 calibration data from the flash 381 382 * ask on IRC! 383 384 === TFTP 385 386 [[tftpserver]] 387 How you get your image onto hardware will vary according to the device, 388 but is likely to involve taking it apart to add wires to serial console 389 pads/headers, then using U-Boot to fetch images over TFTP. The OpenWrt 390 documentation has a 391 https://openwrt.org/docs/techref/hardware/port.serial[good explanation] 392 of what you may expect to find on the device. 393 394 [[tufted]] 395 `tufted` is a rudimentary TFTP server which runs from the command 396 line, has an allowlist for client connections, and follows symlinks, 397 so you can have your device download images direct from the 398 `+./result+` directory without exposing `+/nix/store/+` to the 399 internet or mucking about copying files to `+/tftproot+`. If the 400 permitted device is to be given the IP address 192.168.8.251 you might 401 do something like this: 402 403 [source,console] 404 ---- 405 nix-shell --run "tufted -a 192.168.8.251 result" 406 ---- 407 408 Now add the device and server IP addresses to your configuration: 409 410 [source,nix] 411 ---- 412 boot.tftp = { 413 serverip = "192.168.8.111"; 414 ipaddr = "192.168.8.251"; 415 }; 416 ---- 417 418 and then build the derivation for `+outputs.default+` or 419 `+outputs.mtdimage+` (for which it will be an alias on any device where 420 this is applicable). You should find it has created 421 422 * `+result/firmware.bin+` which is the file you are going to flash 423 * `+result/flash.scr+` which is a set of instructions to U-Boot to 424 download the image and write it to flash after erasing the appropriate 425 flash partition. 426 427 NOTE: TTL serial connections typically have no form of flow control and so 428 don't always like having massive chunks of text pasted into them - and 429 U-Boot may drop characters while it's busy. So don't necessarily expect 430 to copy-paste the whole of `+boot.scr+` into a terminal emulator and 431 have it work just like that. You may need to paste each line one at a 432 time, or even retype it. 433 434 === Running from RAM 435 436 For a faster edit-compile-test cycle, you can build a TFTP-bootable 437 image which boots directly from RAM (using phram) instead of needing 438 to be flashed first. In your device configuration add 439 440 [source,nix] 441 ---- 442 imports = [ 443 ./modules/tftpboot.nix 444 ]; 445 ---- 446 447 and then build `+outputs.tftpboot+`. This creates a file `+result/boot.scr+`, which you can copy and paste into U-Boot to 448 transfer the kernel and filesystem over TFTP and boot the kernel from 449 RAM. 450 451 [[bng]] 452 === Networking 453 454 You probably don't want to be testing a device that might serve DHCP, 455 DNS and routing protocols on the same LAN as you (or your colleagues, 456 employees, or family) are using for anything else, because it will 457 interfere. You also might want to test the device against an "upstream" 458 connection without having to unplug your regular home router from the 459 internet so you can borrow the cable/fibre/DSL. 460 461 `+bordervm+` is included for this purpose. You will need 462 463 * a Linux machine with a spare (PCI or USB) ethernet device which you 464 can dedicate to Liminix 465 * an L2TP service such as https://www.aa.net.uk/broadband/l2tp-service/ 466 467 You need to "hide" the Ethernet device from the host so that QEMU has 468 exclusive use of it. For PCI this means configuring it for VFIO 469 passthru; for USB you need to unload the module(s) it uses. I have 470 this segment in my build machine's `configuration.nix` which you may 471 be able to adapt: 472 473 [source,nix] 474 ---- 475 boot = { 476 kernelParams = [ "intel_iommu=on" ]; 477 kernelModules = [ 478 "kvm-intel" "vfio_virqfd" "vfio_pci" "vfio_iommu_type1" "vfio" 479 ]; 480 481 postBootCommands = '' 482 # modprobe -i vfio-pci 483 # echo vfio-pci > /sys/bus/pci/devices/0000:01:00.0/driver_override 484 ''; 485 blacklistedKernelModules = [ 486 "r8153_ecm" "cdc_ether" 487 ]; 488 }; 489 services.udev.extraRules = '' 490 SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="8153", OWNER="dan" 491 ''; 492 ---- 493 494 Then you can execute `+run-border-vm+` in a `+buildEnv+` shell, which 495 starts up QEMU using the NixOS configuration in 496 `+bordervm-configuration.nix+`. 497 498 Inside the VM 499 500 * your Liminix checkout is mounted under `+/home/liminix/liminix+` 501 * TFTP is listening on the ethernet device and serving 502 `+/home/liminix/liminix+`. The server IP address is 10.0.0.1 503 * a PPPOE-L2TP relay is running on the same ethernet card. When the 504 connected Liminix device makes PPPoE requests, the relay spawns L2TPv2 505 Access Concentrator sessions to your specified L2TP LNS. Note that 506 authentication is expected at the PPP layer not the L2TP layer, so the 507 PAP/CHAP credentials provided by your L2TP service can be configured 508 into your test device - bordervm doesn't need to know about them. 509 510 To configure bordervm, you need a file called `+bordervm.conf.nix+` 511 which you can create by copying and appropriately editing 512 `+bordervm.conf-example.nix+` 513 514 NOTE: If you make changes to the bordervm configuration after executing 515 `+run-border-vm+`, you need to remove the `+border.qcow2+` disk image 516 file otherwise the changes won't get picked up. 517 518 == Contributing 519 520 Patches welcome! Also bug reports, documentation improvements, 521 experience reports/case studies etc etc all equally as welcome. 522 523 * if you have an obvious bug fix, new package, documentation 524 improvement or other uncontroversial small patch, send it straight 525 in. 526 527 * if you have a large new feature or design change in mind, please 528 please _get in touch_ to talk about it before you commit time to 529 implementing it. Perhaps it isn't what we were expecting, almost 530 certainly we will have ideas or advice on what it should do or how 531 it should be done. 532 533 Liminix development is not tied to Github or any other particular 534 forge. How to send changes: 535 536 1. Push your Liminix repo with your changes to a git repository 537 somewhere on the Internet that I can clone from. It can be on Codeberg 538 or Gitlab or Sourcehut or Forgejo or Gitea or Github or a bare repo in 539 your own personal web space or any kind of hosting you like. 540 541 2. Email devel@liminix.org with the URL of the repo and the branch 542 name, and we will take a look. 543 544 If that's not an option, I’m also happy for you to send your changes 545 direct to the list itself, as an incremental git bundle or using git 546 format-patch. We'll work it out somehow. 547 548 The main development repo for Liminix is hosted at 549 <https://gti.telent.net/dan/liminix>, with a read-only mirror at 550 <https://github.com/telent/liminix>. If you're happy to use Github 551 then you can fork from the latter to make your changes, but please use 552 the mailing list one of the approved routes to tell me about your changes because I don't regularly go there to check PRs. 553 554 Remember that the <<_code_of_conduct>> applies to all Liminix spaces, 555 and anyone who violates it may be sanctioned or expelled from these 556 spaces at the discretion of the project leadership. 557 558 === Nix language style 559 560 This section describes some Nix language style points that we attempt to 561 adhere to in this repo. Some are more aspirational than actual. 562 563 * indentation and style is according to `nixfmt-rfc-style` 564 * favour `+callPackage+` over raw `+import+` for calling derivations or 565 any function that may generate one - any code that might need `+pkgs+` 566 or parts of it. 567 * prefer `+let inherit (quark) up down strange charm+` over 568 `+with quark+`, in any context where the scope is more than a single 569 expression or there is more than one reference to `+up+`, `+down+` etc. 570 `+with pkgs; [ foo bar baz]+` is OK, 571 `+with lib; stdenv.mkDerivation { ... }+` is usually not. 572 * `+<liminix>+` is defined only when running tests, so don't refer to it 573 in "application" code 574 * the parameters to a derivation are sorted alphabetically, except for 575 `+lib+`, `+stdenv+` and maybe other non-package "special cases" 576 * where a `+let+` form defines multiple names, put a newline after the 577 token `+let+`, and indent each name two characters 578 * to decide whether some code should be a package or a module? Packages 579 are self-contained - they live in `+/nix/store/eeeeeee-name+` and don't 580 directly change system behaviour by their presence or absense. modules 581 can add to `+/etc+` or `+/bin+` or other global state, create services, 582 all that side-effecty stuff. Generally it should be a package unless it 583 can't be. 584 585 === Copyright 586 587 The Nix code in Liminix is MIT-licenced (same as Nixpkgs), but the code 588 it combines from other places (e.g. Linux, OpenWrt) may have a variety 589 of licences. Copyright assignment is not expected: 590 just like when submitting to the Linux kernel you retain the copyright 591 on the code you contribute. 592 593 === Automated builds 594 595 Automated builds are run on each push to the main branch. This tests 596 that (among other things) 597 598 * every device image builds 599 * the build for the “qemu” target is executed with a fake network upstream to test 600 * PPPoE and DHCP service 601 * hostap (wireless gateway) 602 603 You can view the build output at https://build.liminix.org . The tests 604 are defined in ci.nix. 605 606 Unfortunately there's no (easy) way I can make _my_ CI infrastructure 607 run _your_ code, other than merging it. But see <<_running_tests>> 608 for how to exercise the same code locally on your machine. 609 610 611 == Running tests 612 613 You can run all of the tests by evaluating `+ci.nix+`, which is the 614 input I use in Hydra. 615 616 [source,console] 617 ---- 618 nix-build -I liminix=`pwd` ci.nix -A pppoe # run one job 619 nix-build -I liminix=`pwd` ci.nix -A all # run all jobs 620 ---- 621 622 == Troubleshooting 623 624 === Diagnosing unexpectedly large images 625 626 Sometimes you can add a package and it causes the image size to balloon 627 because it has dependencies on other things you didn't know about. Build 628 the `+outputs.manifest+` attribute, which is a JSON representation of 629 the filesystem, and you can run `+nix-store --query+` on it. 630 631 [source,console] 632 ---- 633 nix-build -I liminix-config=path/to/your/configuration.nix \ 634 --arg device "import ./devices/qemu" -A outputs.manifest \ 635 -o manifest 636 nix-store -q --tree manifest 637 ----