2
0

215 Commits

Author SHA1 Message Date
dan d7d2fb361a er, don\'t know? 2024-12-03 19:30:52 +00:00
dan 1f7d6544e3 provide stdout to ppp callback scripts
pppd runs them with 0,1,2 => /dev/null but we actually quite like
seeing errors in the logs
2024-10-17 21:37:08 +01:00
dan 1bca072509 fix chrony pidfile error 2024-10-17 21:35:33 +01:00
dan 7b98724643 turns out we did need usepeerdns 2024-10-17 21:05:16 +01:00
dan b1625763ee ppp service signal readiness only when ip-up has run
as downstream services need e.g. ifname which is not written by ipv6-up
2024-10-16 22:59:01 +01:00
dan 91bdfc2766 remove apparently obsolete rp-pppoe configure setting
this were copied from nixpkgs but perhaps is for an older version of
rp-pppoe because it builds just fine without
2024-10-16 22:56:05 +01:00
dan 14bfebc5c3 enable unloading modules so that scripts work
if we can't unload them then the service that loads them will fail
the second time it's run
2024-10-16 22:54:19 +01:00
dan 0447ac0ff9 did we need MODULE_SIG?
I think this may be a hangover from using backports modules for wlan
2024-10-16 22:53:16 +01:00
dan e35a1514ab send kernel logs to s6 2024-10-16 18:59:42 +01:00
dan 4a0120487c remove usepeerdns - it causes only errors
we handle dns with service outputs anyway
2024-10-16 18:58:34 +01:00
dan 888688ce28 buuld ppp with path to /run 2024-10-16 18:57:26 +01:00
dan 9e3f48768e think 2024-10-14 18:49:10 +01:00
dan 72171021e3 support finish script in longrun 2024-10-10 18:26:14 +01:00
dan 17517dd34f remove KEXEC from base kernel config
we're not using it any more
2024-10-10 18:23:50 +01:00
dan 5112eab4da apply incoming-allowed-ip[46] rules to input as well as forward pkts
this makes it possible to open ports on the router itself
2024-10-10 18:18:23 +01:00
dan e383f1b3d3 obfuscate store path for min-copy-closure
otherwise the systemconfig closure drags in a bunch of build system
things (bash, etc) which we don't want or need to copy to the device
2024-10-10 16:25:00 +01:00
dan da1245432e no more iminix-rebuild 2024-10-09 19:34:55 +01:00
dan 541b1c61c2 ensure $toplevel is path in /nix/store 2024-10-09 18:59:33 +01:00
dan 55c7410a55 add result/install.sh to systemConfiguration output
this makes it possible to install a systemconfig instead of
having to use nix-shell (which is very slow)
2024-10-09 13:35:02 +01:00
dan 0f50648157 don't put hostname in levitate logs
there might not be one
2024-10-08 22:55:39 +01:00
dan f1c260d4f7 make ci.ni "all" a derivation
this is to stop hydra complaining
2024-10-06 18:04:56 +01:00
dan 3d611d3ba2 fix unstable qemu build?
nix-repl> (lib.versionOlder "24.11pre-git" "24.11")
true

nix-repl> (lib.versionOlder "24.11pre-git" "24.10")
false

n
2024-10-06 18:04:48 +01:00
dan e6b7d86381 sort lines 2024-10-06 17:53:34 +01:00
dan 83fbffb39b catch another uncaught-logs 2024-10-06 17:53:09 +01:00
dan f8c579b41e add CI "all" target 2024-10-06 17:52:59 +01:00
dan ca9efc4b26 simplify CI
* I didn't know what I was doing when I set up Hydra

* it's not certain that I do now either, but hey ho
2024-10-06 15:55:01 +01:00
dan 336fc7e495 think 2024-10-06 14:27:45 +01:00
dan 4cc0add2ad update refs to uncaught-logs in docs/tests 2024-10-06 13:46:14 +01:00
dan 2d7e6188ac log shipping service now gets logs on stdin
instead of having to open the unix socket
2024-10-06 13:26:58 +01:00
dan b9999857cb longrun: don't add logger if producer-for is already set 2024-10-06 13:13:04 +01:00
dan ba03ddeb38 border-vm: add tang service 2024-10-06 12:38:06 +01:00
dan 493c5f69d7 add module for certifix-client 2024-10-06 11:27:39 +01:00
dan 1a915e91ff add altname to CSR 2024-10-06 10:13:28 +01:00
dan 197e2eb5b1 new package certifix-client uses certifix to sign ssl client cert
this is initially for TLS-enabled logging but would be useful for
anything on a liminix box that wants to talk to a network service in a
"zero trust" setup
2024-10-03 23:00:08 +01:00
dan 7ca822c826 more messing around with lua derivation 2024-10-03 23:00:08 +01:00
dan e5631783e1 add luaossl package with patch for CSR attributes 2024-10-03 23:00:08 +01:00
dan 635590d37a implement log shipping config
to use this, you need config like for example

+  logging.shipping = {
+    enable = true;
+    service = longrun {
+      name = "ship-logs";
+      run = let path = lib.makeBinPath (with pkgs; [ s6 s6-networking s6 execline ]);
+            in ''
+        PATH=${path}:$PATH
+        s6-ipcserver -1 ${config.logging.shipping.socket} \
+        s6-tcpclient 10.0.2.2 19612 \
+        fdmove -c 1 7 cat
+      '';
+    };
+  };

but I think we can reduce the noise a bit if we use an s6-rc pipeline
with an s6-ipcserver on one side and and a (whatever the user wants)
on the other
2024-09-18 22:14:34 +01:00
dan 17630f2678 rename logtee->logtap 2024-09-18 20:58:02 +01:00
dan 707a471bc2 add logtee to catchall logger 2024-09-16 21:30:06 +01:00
dan d3fce5edd4 implement error() for musl 2024-09-16 20:35:23 +01:00
dan 5771108fed improve logtee socket connection warning
* print it less often
* to the correct stream (stdout not stderr)
2024-09-16 20:34:26 +01:00
dan 9e5f2d663d close socket fd if we can't connect it 2024-09-15 22:09:31 +01:00
dan 21eeb1671e print diagnostic when eof on stderr 2024-09-15 21:59:24 +01:00
dan 44762d38fc write start cookie when socket connect succeeds 2024-09-15 21:54:21 +01:00
dan 1f6cfc3679 extract method is_connected 2024-09-15 21:40:05 +01:00
dan 8ec00f1710 improve error message 2024-09-15 21:37:04 +01:00
dan 6a6dd32dea make pollfd array global 2024-09-15 21:32:48 +01:00
dan 9b1fc11a59 logshipper/logtee :copy stdin to stdout & to a unix socket if present
first draft
2024-09-15 19:33:21 +01:00
dan aaa6e353db incz is a very rudimentary log shipper for zinc search
although it probably would work with elasticsearch as well
as zinc is alleged to be ES-compatible

this is just the package and needs hooking into the service/log
infrastructure somehow
2024-09-08 16:38:37 +01:00
dan 69bf6cb5fb write-fennel quote PATH properly
escapeShellArg only quotes if the string contains special
characters, but for a Lua string we must quote unconditionally
2024-09-07 22:31:44 +01:00
dan 9f58e7b926 maybe fix nixpkgs-unstable lua 2024-09-07 00:58:11 +01:00
dan 5a5c27ab9f think 2024-09-06 22:37:49 +01:00
dan 277c91acdf Revert "remove luaposix ref in write-fennel"
This reverts commit a60c2539a6.
2024-09-06 00:33:30 +01:00
dan e0725489ca unbreak pppoe ci job 2024-09-06 00:33:30 +01:00
dan cc47515cf8 watch-outputs remove debug code 2024-09-06 00:13:54 +01:00
dan 464913cc8f tangc use spawn to invoke jose
hopefully we are now deadlock-free
2024-09-06 00:12:45 +01:00
dan e604d628e3 fennel anoia.process.spawn
runs a subprocess and invokes a callback whenever its io
descriptors are ready
2024-09-06 00:11:33 +01:00
dan e2a597589b anoia.fs.find-executable looks for bin in colon-sep list of directories 2024-09-06 00:08:40 +01:00
Raito Bezarius a139a262c1 seedrng: init at 2022.04
Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-09-05 14:18:00 +01:00
dan 6a5fed83dd conditional fetch in json-to-fstree 2024-09-05 11:14:47 +01:00
dan bcf5ab24e8 tidy watch-outputs startup message 2024-09-05 10:11:16 +01:00
dan 32bf80c6fa devout: unlink socket pathname before binding 2024-09-05 10:05:13 +01:00
dan 12275f6896 add more test for table= 2024-09-04 21:21:30 +01:00
dan a60c2539a6 remove luaposix ref in write-fennel 2024-09-04 21:21:02 +01:00
dan 146a2d9ac0 fix startup race/fencepost in watch-ssh-keys
if it starts _after_ the outputs are populated, it should
write the first lot of outputs without waiting for a change
2024-09-04 21:19:51 +01:00
dan 091d863710 extract pppoe/l2tp common code 2024-09-04 12:02:00 +01:00
dan c7bcfbfa34 make pppoe/l2tp more consistent 2024-09-03 22:57:45 +01:00
dan 500a3c1025 make nodefaultroute explicit in ppp 2024-09-03 22:53:13 +01:00
dan 0c0d0eed8a make watch-ssh-keys robust against missing key 2024-09-03 22:51:29 +01:00
dan 699cf97206 improve tangc http error messages 2024-09-03 22:50:55 +01:00
dan cd0093279c think 2024-09-01 10:14:31 +01:00
dan 034d6aacc4 tangc handle non-zero exit from jwe dec
Sometimes it exits non-zero but decrypts the file *anyway*. It only
does this on the device and I haven't been able to reproduce on build,
so this is a workaround until we find the root cause
2024-09-01 09:57:38 +01:00
dan e590c0ad3f secrets subscriber: add provider as dep to controlled service 2024-09-01 09:56:59 +01:00
dan 14abdd9998 tang: notify on ready 2024-08-31 23:24:50 +01:00
dan 6287b92000 fix bugs handling base64 padding 2024-08-31 22:43:25 +01:00
dan d2215d3e56 tangc popen retry on short read 2024-08-31 22:18:23 +01:00
dan 3cf2308bee tangc: stop printing unexpected blank lines 2024-08-31 15:29:10 +01:00
dan 3913989be3 provide string to perform-encryption
instead of letting it read stdin, which I think may have been read
by a subprocess already sometimes?
2024-08-31 15:27:54 +01:00
dan 43e5e6876e improve tangc error messages 2024-08-31 15:22:26 +01:00
dan 7d6c80570c refactor all writeFennelScript calls to use writeFennel directly 2024-08-30 20:57:42 +01:00
dan e745991b9d restart pppoe/l2tp in secrets changes 2024-08-30 20:49:27 +01:00
dan defbfce1fb finish converting outputRef to lambda 2024-08-30 20:46:48 +01:00
dan 0df2c83382 tighten perms on service state directory 2024-08-29 23:56:43 +01:00
dan 01c28de88d think 2024-08-29 23:56:20 +01:00
dan 2bf197cad8 document outputs and secrets 2024-08-29 23:55:32 +01:00
dan a8a19977ca (untested) template service for tang encrypted secrets 2024-08-28 22:32:26 +01:00
dan 8a9284af1e think 2024-08-28 22:23:00 +01:00
dan 7351e143c5 remove redundant sourcing of ${serviceFns}
this is done by the oneshot and longrun functions
2024-08-28 21:28:27 +01:00
dan 283c3154a7 missing file in s6-rc-up-tree test fixture 2024-08-28 21:18:54 +01:00
dan 34f37d60d9 missed adding this 2024-08-28 20:56:52 +01:00
dan fe7b092075 (untested) http basic auth for outboard secrets 2024-08-28 20:53:59 +01:00
dan b56f121e04 fetch lua glue: handle missing content-length 2024-08-28 19:52:00 +01:00
dan d5d621f310 rename http-fstree => json-to-fstree
it works for file urls as well, not just http
2024-08-28 16:36:49 +01:00
dan da95a9fa62 tangc support encryption 2024-08-28 18:55:20 +01:00
dan 85071c88e7 remove argv0 from calls to jose 2024-08-28 11:16:43 +01:00
dan 74093b7ee3 josep! runs jose without json parsing the output 2024-08-28 08:13:50 +01:00
dan 41733e58d6 remove unused code, tidy string parsing 2024-08-28 07:20:07 +01:00
dan 9041d5d63a add jose! fn to reduce error-checking boilerplate 2024-08-28 07:10:47 +01:00
dan 001ebdc601 remove unused requires 2024-08-28 06:52:04 +01:00
dan 1f97409474 add popen2 to anoia.fs 2024-08-28 06:49:43 +01:00
dan a41839f3d1 clevis-decrypt-tang in fennel
needs a lot of tidying up, but works on my test file
2024-08-28 01:37:44 +01:00
dan ff76d854fc extend libfetch lua glue to other HTTP methods 2024-08-28 01:37:02 +01:00
dan 81a6480a4f anoia add base64 deode 2024-08-27 22:42:03 +01:00
dan c7164a6f4a sshd can use outputRef for authorized_keys 2024-08-25 16:35:50 +01:00
dan 83ca86fe42 keys in service output tree are strings 2024-08-25 15:59:24 +01:00
dan 1b4106e2a3 ssh-keys service, draft 2024-08-25 15:09:31 +01:00
dan 89912c766b nixpkgs 24.11 qemu does not expect texinfo 2024-08-25 14:23:29 +01:00
dan 9828b007ae watch-ssh-keys turns secrets-service into authorized_keys files 2024-08-24 23:25:32 +01:00
dan f34abc85ae add macros param to write-fennel 2024-08-24 23:19:46 +01:00
dan b475a680fb define-tests macro, evals body only when inside fennelrepl --test 2024-08-24 22:26:25 +01:00
dan 43612af71a anoia: %% is alias for string.formt 2024-08-24 13:56:54 +01:00
dan 5695c47496 add dig to anoia 2024-08-23 23:27:29 +01:00
dan e3ec514710 think 2024-08-23 23:27:17 +01:00
dan 99f68e5421 destructure params in ssh service 2024-08-23 23:13:49 +01:00
dan 9c30b6f882 change output references from attrset to lambda
this is so that we can distinguish a ref from a literal parameter that
might be a attrset
2024-08-23 22:25:57 +01:00
dan dd75322c10 think 2024-08-23 21:45:18 +01:00
dan 869a508c0a add authorizedKeys option to ssh service
this has no apparent use as it stands, but opens the door to
having the keys managed by an external secrets service
2024-08-23 20:35:07 +01:00
dan e835473945 patch dropbear to add -U option 2024-08-23 19:58:05 +01:00
dan 055268d5d2 upgrade dropbear 2024-08-23 19:57:10 +01:00
dan ff38bcacbb improve devout error reporting 2024-08-21 23:24:13 +01:00
dan a6128955e7 ppp modules: permit (mostly) same params for l2tp as pppoe
this also means that l2tp can use secrets for username/password
2024-08-21 23:10:28 +01:00
dan 531cb113be devout needs a longer startup timeout
seems to be taking around 40 seconds now, would be worth digging in to
find out why
2024-08-21 23:09:11 +01:00
dan daede666cb in router-with-l2tp use secrets for ppp username/password 2024-08-21 00:17:53 +01:00
dan 2992771c7e pppoe allow secrets for username/password 2024-08-21 00:17:22 +01:00
dan 4cc82e1502 liminix.types.replacable is a string or ref to an output 2024-08-21 00:16:14 +01:00
dan 21f2320d86 inline method 2024-08-20 23:26:11 +01:00
dan d40ada4251 use structured ppp params in ppp test 2024-08-20 23:25:31 +01:00
dan 4053ea9481 secrets/subscriber implement different restart types 2024-08-20 22:56:26 +01:00
dan 54d3415885 pppoe convert to using a config file
mostly for ease of implementation but does mean we don't
have username/password secrets on the command line
2024-08-20 22:55:30 +01:00
dan 264d83c98d move some secret-watching stuff from hostapd to secrets 2024-08-20 21:49:11 +01:00
dan 97defc2076 hostapd: get secrets service/path from attrs 2024-08-17 22:25:30 +01:00
dan ddaa5476d3 override clevis derivation (experimental) 2024-08-15 23:02:54 +01:00
dan bcd9d56624 start devout after mdevd
not 100% sure that there's a dependency but it's plausible, and
would explain the observed occasional failure to start at boot
2024-08-15 23:01:29 +01:00
dan e2c883356c add secrets-subscriber service, make hostapd use it 2024-08-15 23:00:41 +01:00
dan d79a941504 new package watch-outputs and example of its use 2024-08-14 22:58:17 +01:00
dan 2f82e0dab8 hostapd set permissions on dir in /run/ 2024-08-14 22:57:02 +01:00
dan fc03965915 hostapd literal_or_output use an attrset for dispatch 2024-08-14 22:56:01 +01:00
dan d2d3af2587 outboard secrets: loop in service
if we just quit and expect s6 to restart us, the finish script
wipes our outputs and anything with an inotify watch gets confused
2024-08-14 22:41:56 +01:00
dan 310ac30f24 http-fstree needs to write state and .lock for anoia.svc 2024-08-14 22:39:41 +01:00
dan 45a7f96bd4 anoia table= compares tables 2024-08-14 22:36:28 +01:00
dan 79445fd962 support multi-arg assoc 2024-08-14 22:34:37 +01:00
dan a9ddd78482 think 2024-08-12 22:59:03 +01:00
dan 4fb8253e57 first pass at outboard secrets
- a module to fetch them with http(s)
- a service using templating to consume them
- update an example to use it

needs service restarts
needs other services to use the template mechanism
needs tidying up
2024-08-12 22:57:21 +01:00
dan ff3a1905a5 pass service to output fn in output-template
instead of on command line
2024-08-12 22:53:07 +01:00
dan 3c353e4aff support json quoting in output-template 2024-08-10 23:42:08 +01:00
dan ba21384fde new: output-template interpolates output values into config file 2024-08-10 23:06:47 +01:00
dan 2480fdef5b set up nginx on bordervm for testing outboard secrets 2024-08-10 23:05:50 +01:00
dan 409c1cfb16 think 2024-08-10 23:05:15 +01:00
dan 9767078878 add the example used in the video 2024-08-08 19:24:58 +01:00
dan d760c2d27b http-fstree downloads a json file and converts to service outputs 2024-08-08 15:35:11 +01:00
dan 1e139c22fd think 2024-08-08 15:21:24 +01:00
dan a1ff07b063 add rxi/json lua module 2024-08-08 15:05:26 +01:00
dan 9550772cec add lua binding to fetch-freebsd 2024-08-08 15:05:03 +01:00
dan 64cd1626c6 new package fetch-freebsd: small http(s) client library
[*] smaller than curl, maybe not maximally small
2024-08-08 11:38:38 +01:00
dan eb79928b37 anoia.svc allow writing outputs 2024-08-08 11:37:50 +01:00
dan 0a629df48d anoia.fs: improve error messages 2024-08-08 11:36:47 +01:00
dan 64afd18e2a why does this fail on hydra? 2024-08-06 23:18:39 +01:00
dan 47e96ddc15 think 2024-08-06 18:43:49 +01:00
dan 5db9d7269e ppoe structured options are optional 2024-08-06 18:43:27 +01:00
dan 985df8792d overlay: handle cross-only overrides consistently 2024-08-06 18:42:58 +01:00
dan 528afae8b1 doc: punctuate 2024-08-06 14:15:57 +01:00
dan 384835c89d admin doc: updte round-robin, explain health check 2024-08-06 14:14:52 +01:00
dan 5051625d31 mention health check in docs 2024-07-30 22:53:21 +01:00
dan c4d00e062a add health check service and example that uses it 2024-07-30 22:37:43 +01:00
dan 8fa3443923 Revert "anoia.svc use timeout for inotify"
This reverts commit eca8e37e7a.
2024-07-30 17:37:38 +01:00
dan 8091e207b6 some notes on controlled services 2024-07-28 22:57:23 +01:00
dan 39020607ad rename service-trigger rule to match service name 2024-07-28 22:35:37 +01:00
dan fe735408a1 v:address is nil if missing, but code expects an array 2024-07-27 17:40:32 +01:00
dan a9d1582b53 remove unused arg 2024-07-26 23:41:50 +01:00
dan eca8e37e7a anoia.svc use timeout for inotify
in case we miss a message, check the directory every 5s
anyway
2024-07-26 23:40:40 +01:00
dan d300373b96 anoia fs.dir use case not match
match was accidentally pinning the return from readdir against the
function parameter. Which didn't work.
2024-07-26 23:37:40 +01:00
dan 70ca7fac17 elfutils is reqd by iproute2 (for bpf?), build sans kitchen sink 2024-07-24 22:07:58 +01:00
dan 79a3a45061 build iproute2 without rb to avoid stdatomic 2024-07-24 21:13:55 +01:00
dan 612d6d7a51 build openssl without threads to avoid stdatomic 2024-07-24 21:12:52 +01:00
dan e1ae986cf6 convert l2tp example to use gateway profile 2024-07-23 09:31:34 +01:00
dan bce0c7ffb6 rename services.dhcpc in l2tp example
it's only used to get the address of the l2tp server, not for
name lookups in general
2024-07-23 09:31:34 +01:00
dan 28ca1e68ab wwan module needs mdevd 2024-07-23 09:31:34 +01:00
dan acf33a100f think 2024-07-23 09:31:34 +01:00
dan 7f9cae9d5c generalise profile.gateway.wan so not just pppoe 2024-07-23 09:31:34 +01:00
dan 3012c91b47 executive decision: rotuer example should build on gl-ar750 2024-07-23 09:31:34 +01:00
dan 1edf20c08f fix whitespace 2024-07-23 09:31:34 +01:00
dan 7195cb10ce add structured config for common pppoe options 2024-07-23 09:31:34 +01:00
dan 135a445672 restore param removed by deadnix
dochain is called with `family` even if it never uses it
2024-07-16 20:41:21 +01:00
dan 3899daee56 create a module for round-robin 2024-07-15 22:37:37 +01:00
dan b17f623d03 need insmod when we habve kmodloader 2024-07-15 22:35:26 +01:00
dan df395a4d5d finish moving pkgs.linimix.callService to config.system 2024-07-15 19:00:08 +01:00
dan 75e9f8210c remove the fixpoint we didn't need 2024-07-15 18:54:04 +01:00
dan 1c3242cab1 doc: swap order of configuration and installation
you can get a device up and running using a lightly edited example
config before you need to read all the reference info, so let's
have the documentation in that order.
2024-07-14 12:26:07 +01:00
dan 44ea683391 think 2024-07-14 12:08:02 +01:00
dan 725d8b608f huawei-cdc-ncm kernel driver -> module 2024-07-14 12:07:28 +01:00
dan bc9ced5d38 fix doc ref from admin section -> configuration 2024-07-14 11:56:35 +01:00
dan 73ae7788b9 rename wwan-related modules/services
we only currently support huawei e3372/cdc ncm so let's make that
explicit in the naming
2024-07-14 11:53:45 +01:00
dan d34919766a improve reinstallation docs 2024-07-12 18:38:04 +01:00
dan 2fe0cd2f48 add first draft instructions for using Levitate 2024-07-12 00:17:25 +01:00
dan 241f1013ed add new Installation guide
move the u-boot/serial stuff here from development, as the
reality of Liminix development in 2024 is that serial connection
is still the smoothest installation method
2024-07-11 23:31:00 +01:00
dan 2ce361d4e3 think 2024-07-11 09:39:38 +01:00
dan 3f8cc24dcc fix most doc warnings 2024-07-10 23:36:24 +01:00
dan 57e3b449f8 proofreading 2024-07-10 21:23:24 +01:00
dan 3964505131 some notes on services 2024-07-10 20:50:08 +01:00
dan 941479b144 use round-robin failiover in l2tp example 2024-07-08 22:01:54 +01:00
dan ac551536da set cwd before exec xl2tpd 2024-07-08 21:56:26 +01:00
dan 6f908156af fix dependency between modem-atz and modeswitch
for values of "fix" more than slightly reminiscent of "kludge"
2024-07-08 21:55:05 +01:00
dan 534a49e827 s6-rc-round-robin
runs services in order, starting the next one when the previous one
dies or fails to start
2024-07-08 21:53:51 +01:00
dan 07a6eb73cd set lcp-echo timeout in l2tp 2024-07-08 21:45:54 +01:00
dan 159bfa3057 make xl2tpd quit when the connections close 2024-07-08 21:44:15 +01:00
dan 8f0ab5be40 enable tail -F 2024-07-08 21:37:07 +01:00
dan 7f9971512d a6-rc-up-tree: handle blocked deps, exit 1 if nothing started 2024-07-08 21:28:31 +01:00
dan f0f6cc80d7 remove dead code 2024-07-08 21:28:11 +01:00
dan afcc6a6436 s6-rc-up-tree pass -b to s6-rc command 2024-07-08 21:27:54 +01:00
dan 2e8e05f31a wip: rewrite s6-rc-up-tree in an actual procgramming language
and write some tests for it, too
2024-07-08 21:27:42 +01:00
dan 143137cbc6 pppoe: set lcp echo failure timeout 2024-07-08 21:25:42 +01:00
dan 8d228f2bef mess with redial 2024-07-08 21:24:44 +01:00
dan 5751058d59 gl-ar750 swap lan and wan
I don't know if I just got it wrong the first time or if something
weird is going on
2024-07-08 21:19:30 +01:00
dan 5ac7e1e9b2 write-fennel: set $PATH if lualinux is available 2024-07-08 21:18:02 +01:00
dan c75452549b think 2024-07-08 21:17:12 +01:00
176 changed files with 6054 additions and 897 deletions
+30
View File
@@ -103,3 +103,33 @@ a bit more useful :-)
};
})
];
2024-07-16
* structured parameters are available for the pppoe service
* The "wan" configuration in modules/profiles/gateway.nix has changed:
instead of passing options that are used to create a pppoe interface,
callers should create a (pppoe or other) interface and pass that as
the value of profile.gateway.wan. For the pppoe case this is now only
very slightly more verbose, and it allows using the gateway profile
with other kinds of upstream.
2024-8-16
As part of implementing log shipping, the default directory for system
logs has beenchanged from /run/uncaught-logs to /run/log
2024-10-09
liminix-rebuild is being deprecated. From hereon in, the preferred way
to do an incremental update on an installed device with a writable
filesystem is to build the systemConfiguration output
nix-build -I liminix-config=hosts/myhost.nix --argstr deviceName turris-omnia -A outputs.systemConfiguration
and then run the generated `install.sh` script
result/install.sh root@192.168.8.1
+1378
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -89,9 +89,25 @@ in {
};
};
services.nginx = {
enable = true;
user = "liminix";
virtualHosts.${config.networking.hostName} = {
root = "/home/liminix";
default = true;
};
};
systemd.services.nginx.serviceConfig.ProtectHome = "read-only";
systemd.services.sshd.wantedBy = pkgs.lib.mkForce [ "multi-user.target" ];
virtualisation = {
forwardPorts = [ {
from = "host";
host.port = 7654;
# guest.address = "10.0.2.15";
guest.port =7654;
} ];
qemu = {
networkingOptions = [ ];
options =
@@ -114,6 +130,12 @@ in {
};
};
};
services.tang = {
enable = true;
ipAddressAllow = [ "10.0.0.0/24" "0.0.0.0/0" ];
};
environment.systemPackages =
let wireshark-nogui = pkgs.wireshark.override { withQt = false ; };
in with pkgs; [
@@ -124,6 +146,7 @@ in {
iptables
usbutils
busybox
clevis
];
security.sudo.wheelNeedsPassword = false;
networking = {
+15 -20
View File
@@ -1,11 +1,6 @@
{
nixpkgs,
unstable,
liminix,
...
}:
let
pkgs = (import nixpkgs { });
pkgs = import <nixpkgs> { };
liminix = <liminix>;
borderVmConf = ./bordervm.conf-example.nix;
inherit (pkgs.lib.attrsets) genAttrs;
devices = [
@@ -21,7 +16,7 @@ let
vanilla = ./vanilla-configuration.nix;
for-device = name:
(import liminix {
inherit nixpkgs borderVmConf;
inherit borderVmConf;
device = import (liminix + "/devices/${name}");
liminix-config = vanilla;
}).outputs.default;
@@ -32,7 +27,7 @@ let
// {
buildEnv =
(import liminix {
inherit nixpkgs borderVmConf;
inherit borderVmConf;
device = import (liminix + "/devices/qemu");
liminix-config = vanilla;
}).buildEnv;
@@ -40,7 +35,7 @@ let
let
json =
(import liminix {
inherit nixpkgs borderVmConf;
inherit borderVmConf;
device = import (liminix + "/devices/qemu");
liminix-config =
{ ... }:
@@ -59,26 +54,26 @@ let
];
src = ./.;
buildPhase = ''
cat ${json} | fennel --correlate doc/parse-options.fnl > doc/modules-generated.rst
cat ${json} | fennel --correlate doc/parse-options-outputs.fnl > doc/outputs-generated.rst
cat ${json} | fennel --correlate doc/parse-options.fnl > doc/modules-generated.inc.rst
cat ${json} | fennel --correlate doc/parse-options-outputs.fnl > doc/outputs-generated.inc.rst
cp ${(import ./doc/hardware.nix)} doc/hardware.rst
make -C doc html
'';
installPhase = ''
mkdir -p $out/nix-support $out/share/doc/
cd doc
cp *-generated.rst $out
cp *-generated.inc.rst hardware.rst $out
ln -s ${json} $out/options.json
cp -a _build/html $out/share/doc/liminix
echo "file source-dist \"$out/share/doc/liminix\"" \
> $out/nix-support/hydra-build-products
'';
};
with-unstable = (import liminix {
nixpkgs = unstable;
inherit borderVmConf;
device = import (liminix + "/devices/qemu");
liminix-config = vanilla;
}).outputs.default;
};
in jobs
in jobs //
{
all = pkgs.mkShell {
name = "all tests";
contents = pkgs.lib.collect pkgs.lib.isDerivation jobs;
};
}
+2 -2
View File
@@ -2,14 +2,13 @@
deviceName ? null,
device ? (import ./devices/${deviceName}),
liminix-config ? <liminix-config>,
nixpkgs ? <nixpkgs>,
borderVmConf ? ./bordervm.conf.nix,
imageType ? "primary",
}:
let
overlay = import ./overlay.nix;
pkgs = import nixpkgs (
pkgs = import <nixpkgs> (
device.system
// {
overlays = [ overlay ];
@@ -34,6 +33,7 @@ let
./modules/busybox.nix
./modules/hostname.nix
./modules/kernel
./modules/klogd.nix
device.module
liminix-config
./modules/s6
+23 -8
View File
@@ -114,23 +114,38 @@
eraseBlockSize = 65536;
};
rootDevice = "/dev/mtdblock5";
dts = {
src = "${openwrt.src}/target/linux/ath79/dts/qca9531_glinet_gl-ar750.dts";
includes = [
"${openwrt.src}/target/linux/ath79/dts"
];
};
dts =
let
upstream = {
src = "${openwrt.src}/target/linux/ath79/dts/qca9531_glinet_gl-ar750.dts";
includes = [
"${openwrt.src}/target/linux/ath79/dts"
];
};
cppDtSearchFlags = builtins.concatStringsSep " " (map (f: "-I${f}") upstream.includes);
dtcSearchFlags = builtins.concatStringsSep " " (map (f: "-i${f}") upstream.includes);
patched = pkgs.runCommand "patch-dts" {
nativeBuildInputs = with pkgs.pkgsBuildBuild; [pkgs.stdenv.cc dtc ];
}
''
echo patching dts
${pkgs.stdenv.cc.targetPrefix}cpp -nostdinc -x assembler-with-cpp ${cppDtSearchFlags} -undef -D__DTS__ -o dts.tmp ${upstream.src}
dtc -I dts ${dtcSearchFlags} -o $out -O dts dts.tmp
'';
in upstream // { src = patched; };
networkInterfaces =
let inherit (config.system.service.network) link;
in {
lan = link.build {
ifname = "lan";
devpath = "/devices/platform/ahb/19000000.eth";
devpath = "/devices/platform/ahb/1a000000.eth";
};
wan = link.build {
ifname = "wan";
devpath = "/devices/platform/ahb/1a000000.eth";
devpath = "/devices/platform/ahb/19000000.eth";
};
wlan = link.build {
ifname = "wlan0";
+1 -1
View File
@@ -13,7 +13,7 @@
GL.iNet GL-MT300N-v2
********************
The GL-MT300N-v2 "Mango" is is very similar to the :ref:`MT300A <GL.iNet GL-MT300A>, but is
The GL-MT300N-v2 "Mango" is is very similar to the :ref:`gl-mt300a`, but is
based on the MT7628 chipset instead of MT7620. It's also marginally cheaper
and comes in a yellow case not a blue one. Be sure your device is
v2 not v1, which is a different animal and has only half as much RAM.
+1 -1
View File
@@ -19,7 +19,7 @@
ARM targets differ from MIPS in that the kernel format expected
by QEMU is an "Image" (raw binary file) rather than an ELF
file, but this is taken care of by :command:`run.sh`. Check the
documentation for the :ref:`QEMU` (MIPS) target for more information.
documentation for the :ref:`qemu` target for more information.
'';
+249 -128
View File
@@ -4,143 +4,134 @@ System Administration
Services on a running system
****************************
* add an s6-rc cheatsheet here
Liminix services are built on s6-rc, which is itself layered on s6.
Services are defined at build time in your configuration (see
:ref:`configuration-services` for information) and can't be added
to/changed at runtime, but to monitor
events or diagnose problems you may need to inspect them on the
running system. Here are some of the most commonly used s6,-rc
commands:
.. list-table:: Service management quick reference
:widths: 55 45
:header-rows: 1
* - What
- How
* - List all running services
- ``s6-rc -a list``
* - List all services that are **not** running
- ``s6-rc -da list``
* - List services that ``wombat`` depends on
- ``s6-rc-db dependencies wombat``
* - ... transitively
- ``s6-rc-db all-dependencies wombat``
* - List services that depend on service ``wombat``
- ``s6-rc-db -d dependencies wombat``
* - ... transitively
- ``s6-rc-db -d all-dependencies wombat``
* - Stop service ``wombat`` and everything depending on it
- ``s6-rc -d change wombat``
* - Start service ``wombat`` (but not any services depending on it)
- ``s6-rc -u change wombat``
* - Start service ``wombat`` and all* services depending on it
- ``s6-rc-up-tree wombat``
Flashing and updating
*********************
:command:`s6-rc-up-tree` brings up a service and all services that
depend on it, except for any services that depend on a "controlled"
service that is not currently running. Controlled services are not
started at boot time but in response to external events (e.g. plugging
in a particular piece of hardware) so you probably don't want to be
starting them by hand if the conditions aren't there.
A service may be **up** or **down** (there are no intermediate states
like "started" or "stopping" or "dying" or "cogitating"). Some (but
not all) services have "readiness" notifications: the dependents of a
service with a readiness notification won't be started until the
service signals (by writing to a nominated file descriptor) that it's
prepared to start work. Most services defined by Liminix also have a
``timeout-up`` parameter, which means that if a service has readiness
notifications and doesn't become ready in the allotted time (defaults
20 seconds) it will be terminated and its state set to **down**.
If the process providing a service dies, it will be restarted
automatically. Liminix does not automatically set it to **down**.
Flashing from Liminix
=====================
(If the process providing a service dies without ever notifying
readiness, Liminix will restart it as many times as it has to until the
timeout period elapses, and then stop it and mark it down.)
The flash procedure from an existing Liminix-system has two steps.
First we reboot the device (using "kexec") into an "ephemeral"
RAM-based version of the new configuration, then when we're happy it
works we can flash the image - and if it doesn't work we can reboot
the device again and it will boot from the old image.
Controlled services
===================
**Controlled** services are those which are started/stopped on demand
by a **controller** (another service) instead of being started at boot
time. For example:
Building the RAM-based image
----------------------------
* ``svc.uevent-rule.build`` creates a controlled service which is
active when a particular hardware device (identified by uevent/sysfs
directory) is present.
To create the ephemeral image, build ``outputs.kexecboot`` instead of
``outputs.default``. This generates a directory containing the root
filesystem image and kernel, along with an executable called `kexec`
and a `boot.sh` script that runs it with appropriate arguments.
* ``svc.round-robin.build`` creates a service controller that
invokes two or more services in turn, running the next one when the
process providing the previous one exits. We use this for failover
from one network connection to a backup connection, for example.
For example
* ``svc.health-check.build`` creates a service controller that
runs a controlled service and periodically tests whether it is
healthy by running an external health check command or script. If the
check command repeatedly fails, the controlled service is
restarted.
The Configuration section of the manual describes controlled
services in more detail. Some operational considerations
* ``round-robin`` detects a service status by looking at its
:file:`outputs` directory, so it won't work unless the service
creates some outputs. This is considered a bug and will be
fixed in a future release
* ``health-check`` works for longruns but not for oneshots, as it
internally relies on ``s6-svc`` to restart the process
Logs
====
Logs for all services are collated into :file:`/run/log/current`.
The log file is rotated when it reaches a threshold size, into another
file in the same directory whose name contains a TAI64 timestamp.
Each log line is prefixed with a TAI64 timestamp and the name of the
service, if it is a longrun. If it is a oneshot, a timestamp and the
name of some other service. To convert the timestamp into a
human-readable format, use :command:`s6-tai64nlocal`.
.. code-block:: console
nix-build -I liminix-config=./examples/arhcive.nix \
--arg device "import ./devices/gl-ar750"
-A outputs.kexecboot && \
(tar chf - result | ssh root@the-device tar -C /run -xvf -)
and then login to the device and run
.. code-block:: console
cd /run/result
sh ./boot.sh .
# ls -l /run/log/
-rw-r--r-- 1 0 lock
-rw-r--r-- 1 0 state
-rwxr--r-- 1 98059 @4000000000025cb629c311ac.s
-rwxr--r-- 1 98061 @40000000000260f7309c7fb4.s
-rwxr--r-- 1 98041 @40000000000265233a6cc0b6.s
-rwxr--r-- 1 98019 @400000000002695d10c06929.s
-rwxr--r-- 1 98064 @4000000000026d84189559e0.s
-rwxr--r-- 1 98055 @40000000000271ce1e031d91.s
-rwxr--r-- 1 98054 @400000000002760229733626.s
-rwxr--r-- 1 98104 @4000000000027a2e3b6f4e12.s
-rwxr--r-- 1 98023 @4000000000027e6f0ed24a6c.s
-rw-r--r-- 1 42374 current
# tail -2 /run/log/current
@40000000000284f130747343 wan.link.pppoe Connect: ppp0 <--> /dev/pts/0
@40000000000284f230acc669 wan.link.pppoe sent [LCP ConfReq id=0x1 <asyncmap 0x0> <magic 0x667a9594> <pcomp> <accomp>]
# tail -2 /run/log/current | s6-tai64nlocal
1970-01-02 21:51:45.828598156 wan.link.pppoe sent [LCP ConfReq id=0x1 <asyncmap 0x0> <magic 0x667a9594> <pcomp> <accom
p>]
1970-01-02 21:51:48.832588765 wan.link.pppoe sent [LCP ConfReq id=0x1 <asyncmap 0x0> <magic 0x667a9594> <pcomp> <accom
p>]
This will load the new kernel and map the root filesystem into a RAM
disk, then start executing the new kernel. *This is effectively a
reboot - be sure to close all open files and finish anything else
you were doing first.*
If the new system crashes or is rebooted, then the device will revert
to the old configuration it finds in flash.
Building the second (permanent) image
-------------------------------------
While running in the kexecboot system, you can build the permanent
image and copy it to the device with :command:`ssh`
.. code-block:: console
build-machine$ nix-build -I liminix-config=./examples/arhcive.nix \
--arg device "import ./devices/gl-ar750"
-A outputs.default && \
(tar chf - result | ssh root@the-device tar -C /run -xvf -)
build-machine$ tar chf - result/firmware.bin | \
ssh root@the-device tar -C /run -xvf -
Next you need to connect to the device and locate the "firmware"
partition, which you can do with a combination of :command:`dmesg`
output and the contents of :file:`/proc/mtd`
.. code-block:: console
<5>[ 0.469841] Creating 4 MTD partitions on "spi0.0":
<5>[ 0.474837] 0x000000000000-0x000000040000 : "u-boot"
<5>[ 0.480796] 0x000000040000-0x000000050000 : "u-boot-env"
<5>[ 0.487056] 0x000000050000-0x000000060000 : "art"
<5>[ 0.492753] 0x000000060000-0x000001000000 : "firmware"
# cat /proc/mtd
dev: size erasesize name
mtd0: 00040000 00001000 "u-boot"
mtd1: 00010000 00001000 "u-boot-env"
mtd2: 00010000 00001000 "art"
mtd3: 00fa0000 00001000 "firmware"
mtd4: 002a0000 00001000 "kernel"
mtd5: 00d00000 00001000 "rootfs"
Now run (in this example)
.. code-block:: console
flashcp -v firmware.bin /dev/mtd3
"I know my new image is good, can I skip the intermediate step?"
----------------------------------------------------------------
In addition to giving you a chance to see if the new image works, this
two-step process ensures that you're not copying the new image over
the top of the active root filesystem. Sometimes it works, but you
will at least need physical access to the device to power-cycle it
because it will be effectively frozen afterwards.
Flashing from the boot monitor
==============================
If you are prepared to open the device and have a TTL serial adaptor
of some kind to connect it to, you can probably use U-Boot and a TFTP
server to download and flash the image. This is quite
hardware-specific, and sometimes involves soldering: please refer
to :ref:`serial`.
Flashing from OpenWrt
=====================
.. CAUTION:: Untested! A previous version of these instructions
(without the -e flag) led to bricking the device
when flashing a jffs2 image. If you are reading
this message, nobody has yet reported on whether the
new instructions are any better.
If your device is running OpenWrt then it probably has the
:command:`mtd` command installed. After transferring the image onto the
device using e.g. :command:`ssh`, you can run it as follows:
.. code-block:: console
mtd -e -r write /tmp/firmware.bin firmware
The options to this command are for "erase before writing" and "reboot
after writing".
For more information, please see the `OpenWrt manual <https://openwrt.org/docs/guide-user/installation/sysupgrade.cli>`_ which may also contain (hardware-dependent) instructions on how to flash an image using the vendor firmware - perhaps even from a web interface.
Updating an installed system (JFFS2)
************************************
@@ -165,20 +156,37 @@ Note that this only copies the package to the device: it doesn't update
any profile to add it to ``$PATH``
.. _rebuilding the system:
Rebuilding the system
=====================
:command:`liminix-rebuild` is the Liminix analogue of :command:`nixos-rebuild`, although its operation is a bit different because it expects to run on a build machine and then copy to the host device. Run it with the same ``liminix-config`` and ``device`` parameters as you would run :command:`nix-build`, and it will build any new/changed packages and then copy them to the device using SSH. For example:
Liminix has a mechanism for in-place updates of a running system which
is analogous to :command:`nixos-rebuild`, but its operation is a
bit different because it expects to run on a build machine and then
copy to the host device. To use this, build the `outputs.systemConfiguration`
target and then run the :command:`result/install.sh` script it generates.
.. code-block:: console
liminix-rebuild root@the-device -I liminix-config=./examples/rotuer.nix --arg device "import ./devices/gl-ar750"
nix-build -I liminix-config=./my-configuration.nix \
--arg device "import ./devices/mydevice" \
-A outputs.systemConfiguration
./result/install.sh root@the-device
This will
The install script uses min-copy-closure to copy new or changed
packages to the device, then (perhaps) reboots it. The reboot
behaviour can be affected by flags:
* `--no-reboot` will cause it not to reboot at all, if you would
rather do that yourself. Note that none of the newly-installed or
updated services will be running until you do.
* `--fast` causes it tn not do a full reboot, but instead to restart
only the services that have been changed. This will restart all of
the services that have updated store paths (and anything that
depends on them), but will not affect services that haven't changed.
* build anything that needs building
* copy new or changed packages to the device
* reboot the device
It doesn't delete old packages automatically: to do that run
:command:`min-collect-garbage`, which will delete any packages not in
@@ -196,3 +204,116 @@ Caveats
nixpkgs).
* it cannot upgrade the kernel, only userland
.. _levitate:
Reinstalling on a running system
********************************
Liminix is initially installed from a monolithic
:file:`firmware.bin` - and unless you're running a writable
filesystem, the only way to update it is to build and install a whole
new :file:`firmware.bin`. However, you probably would prefer not to
have to remove it from its installation site, unplug it from the
network and stick serial cables in it all over again.
It is not (generally) safe to install a new firmware onto the flash
partitions that the active system is running on. To address this we
have :command:`levitate`, which a way for a running Liminix system to
"soft restart" into a ramdisk running only a limited set of services,
so that the main partitions can then be safely flashed.
Configuration
=============
Levitate *needs to be configured when you create the initial system*
to specify which services/packages/etc to run in maintenance
mode. Most likely you want to configure a network interface and an ssh
for example so that you can login to reflash it.
.. code-block:: nix
defaultProfile.packages = with pkgs; [
...
(levitate.override {
config = {
services = {
inherit (config.services) dhcpc sshd watchdog;
};
defaultProfile.packages = [ mtdutils ];
users.root = config.users.root;
};
})
];
Use
===
Connect (with ssh, probably) to the running Liminix system that you
wish to upgrade.
.. code-block:: console
bash$ ssh root@the-device
Run :command:`levitate`. This takes a little while (perhaps a few
tens of seconds) to execute, and copies all config required for
maintenance mode to :file:`/run/maintenance`.
.. code-block:: console
# levitate
Reboot into maintenance mode. You will be logged out
.. code-block:: console
# reboot
Connect to the device again - note that the ssh host key will have changed.
.. code-block:: console
# ssh -o UserKnownHostsFile=/dev/null root@the-device
Check we're in maintenance mode
.. code-block:: console
# cat /etc/banner
LADIES AND GENTLEMEN WE ARE FLOATING IN SPACE
Most services are disabled. The system is operating
with a ram-based root filesystem, making it safe to
overwrite the flash devices in order to perform
upgrades and maintenance.
Don't forget to reboot when you have finished.
Perform the upgrade, using flashcp. This is an example,
your device will differ
.. code-block:: console
# cat /proc/mtd
dev: size erasesize name
mtd0: 00030000 00010000 "u-boot"
mtd1: 00010000 00010000 "u-boot-env"
mtd2: 00010000 00010000 "factory"
mtd3: 00f80000 00010000 "firmware"
mtd4: 00220000 00010000 "kernel"
mtd5: 00d60000 00010000 "rootfs"
mtd6: 00010000 00010000 "art"
# flashcp -v firmware.bin mtd:firmware
All done
.. code-block:: console
# reboot
+3 -3
View File
@@ -7,19 +7,19 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'Liminix'
copyright = '2023, Daniel Barlow'
copyright = '2023-2024 Daniel Barlow'
author = 'Daniel Barlow'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'sphinx.ext.autosectionlabel'
# 'sphinx.ext.autosectionlabel'
]
autosectionlabel_prefix_document = True
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
exclude_patterns = ['*.inc.rst', '_build', 'Thumbs.db', '.DS_Store']
+253 -30
View File
@@ -1,27 +1,8 @@
.. _configuration:
Configuration
#############
Liminix uses the Nix language to provide congruent configuration
management. This means that to change anything about the way in
which a Liminix system works, you make that change in
your :file:`configuration.nix` (or one of the other files it references),
and rerun :command:`nix-build` or :command:`liminix-rebuild` to action
the change. It is not possible (at least, without shenanigans) to make
changes by logging into the device and running imperative commands
whose effects may later be overridden: :file:`configuration.nix`
always describes the entire system and can be used to recreate that
system at any time. You can usefully keep it under version control.
If you are familiar with NixOS, you will notice some similarities
between NixOS and Liminix configuration, and also some
differences. Sometimes the differences are due to the
resource-constrained devices we deploy onto, sometimes due to
differences in the uses these devices are put to.
Configuration taxonomy
**********************
There are many things you can specify in a configuration, but these
are the ones you most commonly need to change:
@@ -86,14 +67,166 @@ domains, or you want to run two SSH daemons on different ports.
don't use the NixOS modules themselves, because the
underlying system is not similar enough for them to work.
.. _configuration-services:
Services
********
We use the `s6-rc service manager <https://www.skarnet.org/software/s6-rc/overview.html>`_ to start/stop/restart services and handle
service dependencies. Any attribute in `config.services` will become
part of the default set of services that s6-rc will try to bring up on
boot.
In Liminix a service is any kind of long-running task or process on
the system, that is managed (started, stopped, and monitored) by a
service supervisor. A typical SOHO router might have services to
* answer DHCP and DNS requests from the LAN
* provide a wireless access point
* connect using PPPoE or L2TP to an upstream network
* start/stop the firewall
* enable/disable IP packet forwarding
* mount filesystems
(Some of these might not be considered services using other
definitions of the term: for example, this L2TP process would be a
"client" in the client/server classification; and enabling packet
forwarding doesn't require any long-lived process - just a setting to
be toggled. However, there is value in being able to use the same
abstractions for all the things to manage them and specify their
dependency relationships - so in Liminix "everything is a service")
The service supervision system enables service health monitoring,
restart of unhealthy services, and failover to "backup" services when
a primary service fails or its dependencies are unavailable. The
intention is that you have a framework in which you can specify policy
requirements like "ethernet wan dhcp-client should be restarted if it
crashes, but if it can't start because the hardware link is down, then
4G ppp service should be started instead".
Any attribute in `config.services` will become part of the default set
of services that s6-rc will try to bring up. Services are usually
started at boot time, but **controlled services** are those that are
required only in particular contexts. For example, a service to mount
a USB backup drive should run only when the drive is attached to the
system. Liminix currently implements three kinds of controlled service:
* "uevent-rule" service controllers use sysfs/uevent to identify when
particular hardware devices are present, and start/stop a controlled
service appropriately.
* the "round-robin" service controller is used for service failover:
it allows you to specify a list of services and runs each of them
in turn until it exits, then runs the next.
* the "health-check" service wraps another service, and runs a "health
check" command at regular intervals. When the health check fails,
indicating that the wrapped service is not working, it is terminated
and allowed to restart.
Runtime secrets (external vault)
================================
Secrets (such as wifi passphrases, PPP username/password, SSH keys,
etc) that you provide as literal values in :file:`configuration.nix`
are processed into into config files and scripts at build time, and
eventually end up in various files in the (world-readable)
:file:`/nix/store` before being baked into a flashable image. To
change a secret - whether due to a compromise, or just as part of to a
routine key rotation - you need to rebuild the configuration and
potentially reflash the affected devices.
To avoid this, you may instead use a "secrets service", which is a
mechanism for your device to fetch secrets from a source external to
the Nix store, and create at runtime the configuration files and
scripts that start the services which require them.
Not every possible parameter to every possible service is configurable
using a secrets service. Parameters which can be configured this way
are those with the type ``liminix.lib.types.replacable``. At the time
this document was written, these include:
* ppp (pppoe and l2tp): ``username``, ``password``
* ssh: ``authorizedKeys``
* hostapd: all parameters (most likely to be useful for ``wpa_passphrase``)
To use a runtime secret for any of these parameters:
* create a secrets service to specify the source of truth for secrets
* use the :code:`outputRef` function in the service parameter to specify the secrets service and path
For example, given you had an HTTPS server hosting a JSON file with the structure
.. code-block:: json
"ssh": {
"authorizedKeys": {
"root": [ "ssh-rsa ....", "ssh-rsa ....", ... ]
"guest": [ "ssh-rsa ....", "ssh-rsa ....", ... ]
}
}
you could use a :file:`configuration.nix` fragment something like this
to make those keys visible to ssh:
.. code-block:: nix
services.secrets = svc.secrets.outboard.build {
name = "secret-service";
url = "http://10.0.0.1/secrets.json";
username = "secrets";
password = "liminix";
interval = 30; # minutes
dependencies = [ config.services.lan ];
};
services.sshd = svc.ssh.build {
authorizedKeys = outputRef config.services.secrets "ssh/authorizedKeys";
};
There are presently two implementations of a secrets service:
Outboard secrets (HTTPS)
------------------------
This service expects a URL to a JSON file containing all the secrets.
You may specify a username and password along with the URL, which are
used if the file is password-protected (HTTP Basic
authentication). Note that this is not a protection against a
malicious local user: the username and password are normal build-time
parameters so will be readable in the Nix store. This is a mitigation
against the URL being accidentally discovered due to e.g. a log file
or error message on the server leaking.
Tang secrets (encrypted local file)
-----------------------------------
Aternatively, secrets may be stored locally on the device, in a file
that has been encrypted using `Tang <https://github.com/latchset/tang>`_.
Tang is a server for binding data to network presence.
This sounds fancy, but the concept is simple. You have some data, but you only want it to be available when the system containing the data is on a certain, usually secure, network.
.. code-block:: nix
services.secrets = svc.secrets.tang.build {
name = "secret-service";
path = "/run/mnt/usbstick/secrets.json.jwe";
interval = 30; # minutes
dependencies = [ config.services.mount-usbstick ];
};
The encryption uses the
same scheme/algorithm as `Clevis <https://github.com/latchset/clevis>`_ : you may use the `Clevis instructions <https://github.com/latchset/clevis?tab=readme-ov-file#pin-tang>`_ to
encrypt the file on another host and then copy it to your Liminix
device, or you can use :command:`tangc encrypt` to encrypt directly on
the device. (That latter approach may pose a chicken/egg problem if
the device needs secrets to boot up and run the services you are
relying on in order to login).
Writing services
================
For the most part, for common use cases, hopefully the services you
need will be defined by modules and you will only have to pass the
@@ -141,11 +274,101 @@ Services may have dependencies: as you see above in the ``cowsayd``
example, it depends on some service called ``config.services.lan``,
meaning that it won't be started until that other service is up.
..
TODO: explain service outputs
Service outputs
===============
Outputs are a mechanism by which a service can provide data which may
be required by other services. For example:
* the DHCP client service can expect to receive nameserver address
information as one of the fields in the response from the DHCP
server: we provide that as an output which a dependent service for a
stub name resolver can use to configure its upstream servers.
* a service that creates a new network interface (e.g. ppp) will
provide the name of the interface (:code:`ppp0`, or :code:`ppp1` or
:code:`ppp7`) as an output so that a dependent service can reference
it to set up a route, or to configure firewall rules.
A service :code:`myservice` should write its outputs as files in
:file:`/run/services/outputs/myservice`: you can look around this
directory on a running Liminix system to see how it's used currently.
Usually we use the :code:`in_outputs` shell function in the
:command:`up` or :command:`run` attributes of the service:
.. code-block:: shell
(in_outputs ${name}
for i in lease mask ip router siaddr dns serverid subnet opt53 interface ; do
(printenv $i || true) > $i
done)
The outputs are just files, so technically you can read them using
anything that can read a file. Liminix has two "preferred"
mechanisms, though:
One-off lookups
---------------
In any context that ends up being evaluated by the shell, use
:code:`output` to print the value of an output
.. code-block:: nix
services.defaultroute4 = svc.network.route.build {
via = "$(output ${services.wan} address)";
target = "default";
dependencies = [ services.wan ];
};
Continuous updates
------------------
The downside of using shell functions in downstream service startup
scripts is that they only run when the service starts up: if a service
output *changes*, the downstream service would have to be restarted to
notice the change. Sometimes this is OK but other times the downstream
has no other need to restart, if it can only get its new data.
For this case, there is the :code:`anoia.svc` Fennel library, which
allows you to write a simple loop which is iterated over whenever a
service's outputs change. This code is from
:file:`modules/dhcp6c/acquire-wan-address.fnl`
.. code-block:: fennel
(fn update-addresses [wan-device addresses new-addresses exec]
;; run some appropriate "ip address [add|remove]" commands
)
(fn run []
(let [[state-directory wan-device] arg
dir (svc.open state-directory)]
(accumulate [addresses []
v (dir:events)]
(update-addresses wan-device addresses
(or (v:output "address") []) system))))
The :code:`output` method seen here accepts a filename (relative
to the service's output directory), or a directory name. It
returns the first line of that file, or for directories it
returns a table (Lua's key/value datastructure, similar to
a hash/dictionary) of the outputs in that directory.
Output design considerations
----------------------------
For preference, outputs should be short and simple, and not require
downstream services to do complicated parsing in order to use them.
Shell commands in Liminix are run using the Busybox shell which
doesn't have the niceties of an advanced shell like Bash let alone
those of a real programming language.
Note also that the Lua :code:`svc` library only reads the first line
of each output.
..
TODO: outputs that change, and services that poll other services
Module implementation
*********************
@@ -192,7 +415,7 @@ To expose a service template in a module, it needs the following:
.. code-block:: nix
config.system.service.cowsay = liminix.callService ./service.nix {
config.system.service.cowsay = config.system.callService ./service.nix {
address = mkOption {
type = types.str;
default = "0.0.0.0";
+4 -67
View File
@@ -88,64 +88,6 @@ time with configurations for RP-PPPoE and/or Accel PPP.`
Hardware devices
****************
.. _serial:
U-Boot and serial shenanigans
=============================
Every device that we have so far encountered in Liminix uses `U-Boot,
the "Universal Boot Loader" <https://docs.u-boot.org/en/latest/>`_ so
it's worth knowing a bit about it. "Universal" is in this context a
bit of a misnomer, though: encountering *mainline* U-Boot is very rare
and often you'll find it is a fork from some version last updated
in 2008. Upgrading U-Boot is more or less complicated depending on the
device and is outside scope for Liminix.
To speak to U-Boot on your device you'll usually need a serial
connection to it. This is device-specific. Usually it involves
opening the box, locating the serial header pins (TX, RX and GND) and
connecting a USB TTL converter to them.
The Rolls Royce of USB/UART cables is the `FTDI cable
<https://cpc.farnell.com/ftdi/ttl-232r-rpi/cable-debug-ttl-232-usb-rpi/dp/SC12825?st=usb%20to%20uart%20cable>`_,
but there are cheaper alternatives based on the PL2303 and CP2102 chipsets. Or
get creative and use the `UART GPIO pins <https://pinout.xyz/>`_ on a Raspberry Pi. Whatever you do, make sure
that the voltages are compatible: if your device is 3.3V (this is
typical but not universal), you don't want to be sending it 5v or
(even worse) 12v.
Run a terminal emulator such as Minicom on the computer at other end
of the link. 115200 8N1 is the typical speed.
.. NOTE::
TTL serial connections typically have no form of flow control and
so don't always like having massive chunks of text pasted into
them - and U-Boot may drop characters while it's busy. So don't
necessarily expect to copy-paste large chunks of text into the
terminal emulator and have it work just like that.
If using Minicom, you may find it helps to bring up the "Termimal
settings" dialog (C^A T), then configure "Newline tx delay" to
some small but non-zero value.
When you turn the router on you should be greeted with some messages
from U-Boot, followed by the instruction to hit some key to stop
autoboot. Do this and you will get to the prompt. If you didn't see
anything, the strong likelihood is that TX and RX are the wrong way
around. If you see garbage, try a different speed.
Interesting commands to try first in U-Boot are :command:`help` and
:command:`printenv`.
To do anything useful with U-Boot you will probably need a way to get
large binary files onto the device, and the usual way to do this is by
adding a network connection and using TFTP to download them. It's
quite common that the device's U-Boot doesn't speak DHCP so it will
need a static LAN address. You might also want to keep it away from
your "real" LAN: see :ref:`bng` for some potentially useful tooling
to use it on an isolated network.
TFTP
====
@@ -290,18 +232,13 @@ Running tests
*************
You can run all of the tests by evaluating :file:`ci.nix`, which is the
input I use in Hydra. Note that it expects Nixpkgs stable `and` unstable
as inputs, because it builds the qemu device against both.
input I use in Hydra.
.. code-block:: console
nix-build --argstr liminix `pwd` --arg nixpkgs "<nixpkgs>" \
--argstr unstable `pwd`/../unstable-nixpkgs/ ci.nix
To run a single named test, use the ``-A`` flag. For example, ``-A pppoe``
nix-build -I liminix=`pwd` ci.nix -A pppoe # run one job
nix-build -I liminix=`pwd` ci.nix -A all # run all jobs
Troubleshooting
***************
+6 -2
View File
@@ -7,11 +7,15 @@ let
n:
let
d = import ../devices/${n}/default.nix;
tag = ".. _${lib.strings.replaceStrings [" "] ["-"] n}:";
d' = {
description = "${n}\n${substring 0 (stringLength n) "********************************"}\n";
description = ''
${n}
${substring 0 (stringLength n) "********************************"}
'';
} // d;
in
d'.description
"${tag}\n\n${d'.description}"
) devices;
in
writeText "hwdoc" ''
+1
View File
@@ -7,6 +7,7 @@ Liminix
intro
tutorial
installation
configuration
admin
development
+211
View File
@@ -0,0 +1,211 @@
Installation
############
Hardware devices vary wildly in their affordances for installing new
operating systems, so it should be no surprise that the Liminix
installation procedure is hardware-dependent. This section contains
generic instructions, but please refer to the documentation for your
device to find whether and how well they apply.
Building a firmware image
*************************
Liminix uses the Nix language to provide congruent configuration
management. This means that to change anything about the way in
which a Liminix system works, you make that change in
your :file:`configuration.nix` (or one of the other files it references),
and rerun :command:`nix-build` to action
the change. It is not possible (at least, without shenanigans) to make
changes by logging into the device and running imperative commands
whose effects may later be overridden: :file:`configuration.nix`
always describes the entire system and can be used to recreate that
system at any time. You can usefully keep it under version control.
If you are familiar with NixOS, you will notice some similarities
between NixOS and Liminix configuration, and also some
differences. Sometimes the differences are due to the
resource-constrained devices we deploy onto, sometimes due to
differences in the uses these devices are put to.
For a more full description of how to configure Liminix, see
:ref:`configuration`. Assuming for the moment that you want a typical
home wireless gateway/router, the best way to get started is to copy
:file:`examples/rotuer.nix` and edit it for your requirements.
.. code-block:: console
$ cp examples/rotuer.nix configuration.nix
$ vi configuration.nix # other editors are available
$ # adjust this next command for your hardware device
$ nix-build -I liminix-config=./configuration.nix \
--arg device "import ./devices/gl-mt300a" -A outputs.default
Usually (not always, *please check the documentation for your device*)
this will leave you with a file :file:`result/firmware.bin`
which you now need to flash to the device.
Flashing from the boot monitor (TFTP install)
*********************************************
If you are prepared to open the device and have a TTL serial adaptor
of some kind to connect it to, you can probably use U-Boot and a TFTP
server to download and flash the image.
This is quite hardware-specific and may even involve soldering - see
the documention for your device. However, it is in some ways the most
"reliable" option: if you can see what's happening (or not happening)
in early boot, the risk of "bricking" is substantially reduced and you
have options for recovering if you misstep or flash a bad image.
.. _serial:
U-Boot and serial shenanigans
=============================
Every device that we have so far encountered in Liminix uses `U-Boot,
the "Universal Boot Loader" <https://docs.u-boot.org/en/latest/>`_ so
it's worth knowing a bit about it. "Universal" is in this context a
bit of a misnomer, though: encountering *mainline* U-Boot is very rare
and often you'll find it is a fork from some version last updated
in 2008. Upgrading U-Boot is more or less complicated depending on the
device and is outside scope for Liminix.
To speak to U-Boot on your device you'll usually need a serial
connection to it. This typically involves opening the box, locating
the serial header pins (TX, RX and GND) and connecting a USB TTL
converter to them.
The Rolls Royce of USB/UART cables is the `FTDI cable
<https://cpc.farnell.com/ftdi/ttl-232r-rpi/cable-debug-ttl-232-usb-rpi/dp/SC12825?st=usb%20to%20uart%20cable>`_,
but there are cheaper alternatives based on the PL2303 and CP2102 chipsets - or you could even
get creative and use the `UART GPIO pins <https://pinout.xyz/>`_ on a Raspberry Pi. Whatever you do, make sure
that the voltages are compatible: if your device is 3.3V (this is
typical but not universal), you don't want to be sending it 5v or
(even worse) 12v.
Run a terminal emulator such as Minicom on the computer at other end
of the link. 115200 8N1 is the typical speed.
.. NOTE::
TTL serial connections often have no flow control and
so don't always like having massive chunks of text pasted into
them - and U-Boot may drop characters while it's busy. So don't
do that.
If using Minicom, you may find it helps to bring up the "Termimal
settings" dialog (C^A T), then configure "Newline tx delay" to
some small but non-zero value.
When you turn the router on you should be greeted with some messages
from U-Boot, followed by the instruction to hit some key to stop
autoboot. Do this and you will get to the prompt. If you didn't see
anything, the strong likelihood is that TX and RX are the wrong way
around. If you see garbage, try a different speed.
Interesting commands to try first in U-Boot are :command:`help` and
:command:`printenv`.
You will also need to configure a TFTP server on a network that's
accessible to the device: how you do that will vary according to which
TFTP server you're using and so is out of scope for this document.
Buildiing and installing the image
==================================
Follow the device-specific instructions for "TFTP install": usually,
the steps are
* build the `outputs.mtdimage` output
* copy :file:`result/firmware.bin` to your TFTP server
* copy/paste the commands in :file:`result/flash.scr` one at a time into the U-Boot command line
* reset the device
You should now see messages from U-Boot, then from the Linux kernel
and eventually a shell prompt.
.. NOTE:: Before you reboot, check which networks the device is
plugged into, and disconnect as necessary. If you've just
installed a DHCP server or anything similar that responds to
broadcasts, you may not want it to do that on the network
that you temporarily connected it to for installing it.
Flashing from OpenWrt
*********************
.. CAUTION:: Untested! A previous version of these instructions
(without the -e flag) led to bricking the device
when flashing a jffs2 image. If you are reading
this message, nobody has yet reported on whether the
new instructions are any better.
If your device is running OpenWrt then it probably has the
:command:`mtd` command installed. Build the `outputs.mtdimage` output
(as you would for a TFTP install) and then transfer
:file:`result/firmware.bin` onto the device using e.g.
:command:`scp`. Now flash as follows:
.. code-block:: console
mtd -e -r write /tmp/firmware.bin firmware
The options to this command are for "erase before writing" and "reboot
after writing".
For more information, please see the `OpenWrt manual <https://openwrt.org/docs/guide-user/installation/sysupgrade.cli>`_ which may also contain (hardware-dependent) instructions on how to flash an image using the vendor firmware - perhaps even from a web interface.
Flashing from Liminix
*********************
If the device is already running Liminix and has been configured with
:command:`levitate`, you can use that to safely flash your new image.
Refer to :ref:`levitate` for an explanation.
If the device is running Liminix but doesn't have :command:`levitate`
your options are more limited. You may attempt to use
:command:`flashcp` but it doesn't always work: as it copies the new
image over the top of the active root filesystem, surprise may ensue.
Consider instead using a serial connection: you may need one anyway
after trying flashcp if it corrupts the image.
flashcp (not generally recommended)
===================================
Connect to the device and locate the "firmware" partition, which you
can do with a combination of :command:`dmesg` output and the contents
of :file:`/proc/mtd`
.. code-block:: console
<5>[ 0.469841] Creating 4 MTD partitions on "spi0.0":
<5>[ 0.474837] 0x000000000000-0x000000040000 : "u-boot"
<5>[ 0.480796] 0x000000040000-0x000000050000 : "u-boot-env"
<5>[ 0.487056] 0x000000050000-0x000000060000 : "art"
<5>[ 0.492753] 0x000000060000-0x000001000000 : "firmware"
# cat /proc/mtd
dev: size erasesize name
mtd0: 00040000 00001000 "u-boot"
mtd1: 00010000 00001000 "u-boot-env"
mtd2: 00010000 00001000 "art"
mtd3: 00fa0000 00001000 "firmware"
mtd4: 002a0000 00001000 "kernel"
mtd5: 00d00000 00001000 "rootfs"
Copy :file:`result/firmware.bin` to the device and now run (in this
example)
.. code-block:: console
flashcp -v firmware.bin /dev/mtd3
+1 -1
View File
@@ -1,4 +1,4 @@
Module options
##############
.. include:: modules-generated.rst
.. include:: modules-generated.inc.rst
+1 -1
View File
@@ -10,4 +10,4 @@ different artefacts, or have different ways to get that artefact
installed. The options available for a particular device are described in
the section for that device.
.. include:: outputs-generated.rst
.. include:: outputs-generated.inc.rst
+1 -1
View File
@@ -16,4 +16,4 @@
(each [_ option (ipairs (sorted-options (yaml.load (io.read "*a"))))]
(when (and (output? option) (not option.internal))
(print (.. ".. _" (string.gsub option.name "%." "-") ":") "\n")
(print option.description)))
(print option.description "\n")))
+6 -3
View File
@@ -71,7 +71,7 @@ stdin/stdout.
You should now see Linux boot messages and after a few seconds be
presented with a root shell prompt. You can run commands to look at
the filesystem, see what processes are running, view log messages (in
:file:/run/uncaught-logs.current), etc. To kill the emulator, press ^P
:file:/run/log/current), etc. To kill the emulator, press ^P
(Control P) then c to enter the "QEMU Monitor", then type ``quit`` at
the ``(qemu)`` prompt.
@@ -293,14 +293,17 @@ the hostname) and then run
.. code-block:: console
nix-shell --run "liminix-rebuild root@address-of-the-device -I liminix-config=./my-router.nix --arg device "import ./devices/gl-ar750""
nix-build -I liminix-config=./my-router.nix \
--arg device "import ./devices/gl-ar750" \
-A outputs.systemConfiguration && \
result/install.sh root@address-of-the-device
(This requires the device to be network-accessible from your build
machine, which for a test/demo system might involve a second network
device in your build system - USB ethernet adapters are cheap - or
a bit of messing around unplugging cables.)
For more information about :code:`liminix-rebuild`, see the manual section :ref:`admin:Rebuilding the system`.
For more information about in-place-updates, see the manual section :ref:`Rebuilding the system`.
Final thoughts
-2
View File
@@ -52,7 +52,6 @@ in rec {
dependencies = [ services.dhcpc ];
name = "resolvconf";
up = ''
. ${serviceFns}
( in_outputs ${name}
for i in $(output ${services.dhcpc} dns); do
echo "nameserver $i" > resolv.conf
@@ -93,7 +92,6 @@ in rec {
secrets_file = oneshot rec {
name = "rsync-secrets";
up = ''
. ${serviceFns}
(in_outputs ${name}
echo "backup:${secrets.rsync_secret}" > secrets)
'';
-1
View File
@@ -137,7 +137,6 @@ in 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
+47 -2
View File
@@ -1,12 +1,14 @@
{ config, pkgs, ... } :
{ config, pkgs, lib, ... } :
let
svc = config.system.service;
inherit (pkgs.liminix.services) longrun oneshot ;
in rec {
imports = [
../modules/network
../modules/dnsmasq
../modules/ssh
../modules/tls-certificate
];
hostname = "hello";
@@ -17,8 +19,51 @@ in rec {
family = "inet"; address ="10.3.0.1"; prefixLength = 16;
};
filesystem =
let inherit (pkgs.pseudofile) file dir symlink;
in dir {
etc = dir {
hosts = {
type = "f";
file = "127.0.0.1 localhost\n10.0.2.2 loaclhost.telent.net\n";
mode = "0444";
};
};
};
services.sshd = svc.ssh.build { };
services.lan-address-for-secrets =
svc.network.address.build {
interface = config.hardware.networkInterfaces.lan;
family = "inet"; address ="10.0.2.15"; prefixLength = 24;
};
# services.client-cert = svc.tls-certificate.certifix-client.build {
# caCertificate = builtins.readFile /var/lib/certifix/certs/ca.crt;
# subject = "C=GB,ST=London,O=Telent,OU=devices,CN=${config.hostname}";
# secret = builtins.readFile ../challengePassword;
# serviceUrl = "https://loaclhost.telent.net:19613/sign";
# };
# logging.shipping = {
# enable = true;
# service = longrun {
# name = "ship-logs";
# dependencies = [ config.services.client-cert ];
# run =
# let path = lib.makeBinPath (with pkgs; [ s6-networking s6 ]);
# in ''
# PATH=${path}:$PATH \
# CAFILE=${/var/lib/certifix/certs/ca.crt} \
# KEYFILE=$(output_path ${services.client-cert} key) \
# CERTFILE=$(output_path ${services.client-cert} cert) \
# s6-tlsclient -k loaclhost.telent.net -h -y loaclhost.telent.net 19612 \
# fdmove -c 1 7 cat
# '';
# };
# };
users.root = {
# the password is "secret". Use mkpasswd -m sha512crypt to
# create this hashed password string
@@ -38,6 +83,6 @@ in rec {
};
defaultProfile.packages = with pkgs; [
figlet
figlet openssl
];
}
-141
View File
@@ -1,141 +0,0 @@
{
config,
pkgs,
lib,
...
}: let
secrets = import ./extneder-secrets.nix;
rsecrets = import ./rotuer-secrets.nix;
# https://support.aa.net.uk/Category:Incoming_L2TP says:
# "Please use the DNS name (l2tp.aa.net.uk) instead of hardcoding an
# IP address; IP addresses can and do change. If you have to use an
# IP, use 194.4.172.12, but do check the DNS for l2tp.aa.net.uk in
# case it changes."
# but (1) we don't want to use the wwan stick's dns as our main
# resolver: it's provided by some mobile ISP and they aren't
# necessarily the best at providing unfettered services without
# deciding to do something weird; (2) it's not simple to arrange
# that xl2tpd gets a different resolver than every other process;
# (3) there's no way to specify an lns address to xl2tpd at runtime
# except by rewriting its config file. So what we will do is lookup
# the lns hostname using the mobile ISP's dns server and then refuse
# to start l2tp unless the expected lns address is one of the
# addresses returned. I think this satisfies "do check the DNS"
lns = { hostname = "l2tp.aaisp.net.uk"; address = "194.4.172.12"; };
inherit (pkgs.liminix.services) oneshot target;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) serviceFns;
svc = config.system.service;
in rec {
boot = {
tftp = {
serverip = "10.0.0.1";
ipaddr = "10.0.0.8";
};
};
imports = [
../modules/cdc-ncm
../modules/network
../modules/vlan
../modules/ssh
../modules/usb.nix
../modules/watchdog
../modules/mount
../modules/ppp
];
hostname = "thing";
services.wwan = svc.wwan.build {
apn = "data.uk";
username = "user";
password = "one2one";
authType = "chap";
};
services.dhcpc = svc.network.dhcp.client.build {
interface = config.services.wwan;
dependencies = [ config.services.hostname ];
};
services.sshd = svc.ssh.build { };
services.resolvconf = oneshot rec {
dependencies = [ services.l2tp ];
name = "resolvconf";
up = ''
. ${serviceFns}
( in_outputs ${name}
for i in ns1 ns2 ; do
ns=$(output ${services.l2tp} $i)
echo "nameserver $ns" >> resolv.conf
done
)
'';
};
filesystem = dir {
etc = dir {
"resolv.conf" = symlink "${services.resolvconf}/.outputs/resolv.conf";
};
};
services.lns-address = let
ns = "$(output_word ${services.dhcpc} dns 1)";
route-to-bootstrap-nameserver = svc.network.route.build {
via = "$(output ${services.dhcpc} router)";
target = ns;
dependencies = [services.dhcpc];
};
in oneshot rec {
name = "resolve-l2tp-server";
dependencies = [ services.dhcpc route-to-bootstrap-nameserver ];
up = ''
(in_outputs ${name}
DNSCACHEIP="${ns}" ${pkgs.s6-dns}/bin/s6-dnsip4 ${lns.hostname} \
> addresses
)
'';
};
services.l2tp =
let
check-address = oneshot rec {
name = "check-lns-address";
up = ''
grep -Fx ${lns.address} $(output_path ${services.lns-address} addresses)
'';
dependencies = [ services.lns-address ];
};
route = svc.network.route.build {
via = "$(output ${services.dhcpc} router)";
target = lns.address;
dependencies = [services.dhcpc check-address];
};
in svc.l2tp.build {
lns = lns.address;
ppp-options = [
"debug" "+ipv6" "noauth"
"name" rsecrets.l2tp.name
"connect-delay" "5000"
"password" rsecrets.l2tp.password
];
dependencies = [config.services.lns-address route check-address];
};
services.defaultroute4 = svc.network.route.build {
via = "$(output ${services.l2tp} peer-address)";
target = "default";
dependencies = [services.l2tp];
};
# defaultProfile.packages = [ pkgs.go-l2tp ];
users.root = {
passwd = lib.mkForce secrets.root.passwd;
openssh.authorizedKeys.keys = secrets.root.keys;
};
}
-2
View File
@@ -60,7 +60,6 @@ in rec {
dependencies = [ services.dhcpc ];
name = "resolvconf";
up = ''
. ${serviceFns}
( in_outputs ${name}
for i in $(output ${services.dhcpc} dns); do
echo "nameserver $i" > resolv.conf
@@ -72,7 +71,6 @@ in rec {
services.growfs = let name = "growfs"; in oneshot {
inherit name;
up = ''
. ${serviceFns}
device=$(grep /persist /proc/1/mountinfo | cut -f9 -d' ')
${pkgs.e2fsprogs}/bin/resize2fs $device
'';
+89 -21
View File
@@ -1,10 +1,8 @@
# This is not part of Liminix per se. This is my "scratchpad"
# configuration for the device I'm testing with.
#
# Parts of it do do things that Liminix eventually needs to do, but
# don't look in here for solutions - just for identifying the
# problems.
# This is an example that uses the "gateway" profile to create a
# "typical home wireless router" configuration suitable for a Gl.inet
# gl-ar750 router. It should be fairly simple to edit it for other
# devices: mostly you will need to attend to the number of wlan and lan
# interfaces
{ config, pkgs, lib, modulesPath, ... } :
let
@@ -18,11 +16,11 @@ let
inherit (secrets) wpa_passphrase;
wmm_enabled = 1;
};
inherit (pkgs.liminix.services) longrun;
in rec {
boot = {
tftp = {
freeSpaceBytes = 3 * 1024 * 1024;
freeSpaceBytes = 2 * 1024 * 1024;
serverip = "10.0.0.1";
ipaddr = "10.0.0.8";
};
@@ -30,21 +28,34 @@ in rec {
imports = [
"${modulesPath}/profiles/gateway.nix"
"${modulesPath}/schnapps"
"${modulesPath}/outputs/btrfs.nix"
"${modulesPath}/outputs/extlinux.nix"
"${modulesPath}/tls-certificate"
];
hostname = "rotuer";
rootfsType = "btrfs";
rootOptions = "subvol=@";
boot.loader.extlinux.enable = true;
# rootfsType = "jffs2";
filesystem =
let inherit (pkgs.pseudofile) file dir symlink;
in dir {
mnt = dir {};
etc = dir {
hosts = {
type = "f";
file = "127.0.0.1 localhost\n10.0.0.1 loaclhost.telent.net\n";
mode = "0444";
};
};
};
profile.gateway = {
lan = {
interfaces = with config.hardware.networkInterfaces;
[
# EDIT: these are the interfaces exposed by the gl.inet gl-ar750:
# if your device has more or differently named lan interfaces,
# specify them here
wlan wlan5
lan0 lan1 lan2 lan3 lan4
lan
];
inherit (secrets.lan) prefix;
address = {
@@ -58,9 +69,17 @@ in rec {
};
};
wan = {
interface = config.hardware.networkInterfaces.wan;
username = secrets.l2tp.name;
password = secrets.l2tp.password;
# wan interface depends on your upstream - could be dhcp, static
# ethernet, a pppoe, ppp over serial, a complicated bonded
# failover ... who knows what else?
interface = svc.pppoe.build {
interface = config.hardware.networkInterfaces.wan;
username = secrets.l2tp.name;
password = secrets.l2tp.password;
};
# once the wan has ipv4 connnectivity, should we run dhcp6
# client to potentially get an address range ("prefix
# delegation")
dhcp6.enable = true;
};
firewall = {
@@ -68,6 +87,10 @@ in rec {
rules = secrets.firewallRules;
};
wireless.networks = {
# EDIT: if you have more or fewer wireless radios, here is where
# you need to say so. hostapd tuning is hardware-specific and
# left as an exercise for the reader :-).
"${secrets.ssid}" = {
interface = config.hardware.networkInterfaces.wlan;
hw_mode = "g";
@@ -99,9 +122,11 @@ in rec {
defaultProfile.packages = with pkgs; [
min-collect-garbage
nftables
strace
tcpdump
# strace
# tcpdump
s6
dtc
# certifix-client
];
programs.busybox = {
@@ -112,4 +137,47 @@ in rec {
FEATURE_FANCY_TAIL = "y";
};
};
services.wan-address-for-secrets =
let
interface = config.hardware.networkInterfaces.wan;
addr =
svc.network.address.build {
inherit interface;
family = "inet"; address ="10.0.0.10"; prefixLength = 24;
};
in svc.network.route.build {
target = "10.0.0.1";
inherit interface;
via = "10.0.0.10";
metric = 1;
dependencies = [ addr ];
};
# services.client-cert = svc.tls-certificate.certifix-client.build {
# caCertificate = builtins.readFile /var/lib/certifix/certs/ca.crt;
# subject = "C=GB,ST=London,O=Telent,OU=devices,CN=${config.hostname}";
# secret = builtins.readFile ../challengePassword;
# serviceUrl = "https://loaclhost.telent.net:19613/sign";
# dependencies = [ services.wan-address-for-secrets ];
# };
logging.pstore = true;
logging.shipping = {
enable = false;
service = longrun {
name = "ship-logs";
dependencies = [ config.services.client-cert ];
run =
let path = lib.makeBinPath (with pkgs; [ s6-networking s6 ]);
in ''
PATH=${path}:$PATH \
CAFILE=${/var/lib/certifix/certs/ca.crt} \
KEYFILE=$(output_path ${services.client-cert} key) \
CERTFILE=$(output_path ${services.client-cert} cert) \
s6-tlsclient -k loaclhost.telent.net -h -y loaclhost.telent.net 19612 \
fdmove -c 1 7 cat
'';
};
};
}
+211
View File
@@ -0,0 +1,211 @@
# A demonstration config for a home/soho router with PPPoE upstream
# and fallback to an L2TP tunnel over a USB WWAN device
{
config,
pkgs,
lib,
...
}: let
secrets = import ./extneder-secrets.nix;
rsecrets = import ./rotuer-secrets.nix;
# https://support.aa.net.uk/Category:Incoming_L2TP says:
# "Please use the DNS name (l2tp.aa.net.uk) instead of hardcoding an
# IP address; IP addresses can and do change. If you have to use an
# IP, use 194.4.172.12, but do check the DNS for l2tp.aa.net.uk in
# case it changes."
# but (1) we don't want to use the wwan stick's dns as our main
# resolver: it's provided by some mobile ISP and they aren't
# necessarily the best at providing unfettered services without
# deciding to do something weird; (2) it's not simple to arrange
# that xl2tpd gets a different resolver than every other process;
# (3) there's no way to specify an lns address to xl2tpd at runtime
# except by rewriting its config file. So what we will do is lookup
# the lns hostname using the mobile ISP's dns server and then refuse
# to start l2tp unless the expected lns address is one of the
# addresses returned. I think this satisfies "do check the DNS"
lns = { hostname = "l2tp.aaisp.net.uk"; address = "194.4.172.12"; };
inherit (pkgs.liminix.services) oneshot longrun target;
inherit (pkgs.liminix) outputRef;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) serviceFns;
svc = config.system.service;
wirelessConfig = {
country_code = "GB";
inherit (rsecrets) wpa_passphrase;
wmm_enabled = 1;
};
in rec {
boot = {
tftp = {
serverip = "10.0.0.1";
ipaddr = "10.0.0.8";
};
};
imports = [
../modules/wwan
../modules/network
../modules/ssh
../modules/usb.nix
../modules/ppp
../modules/round-robin
../modules/health-check
../modules/secrets
../modules/profiles/gateway.nix
];
hostname = "thing";
services.wan-address-for-secrets = svc.network.address.build {
interface = config.hardware.networkInterfaces.wan;
family = "inet"; address ="10.0.0.10"; prefixLength = 24;
};
services.secrets = svc.secrets.outboard.build {
name = "secret-service";
url = "http://10.0.0.1/liminix/examples/real-secrets.json";
username = "demo";
password = "demo";
interval = 5;
dependencies = [ services.wan-address-for-secrets ];
};
services.wwan = svc.wwan.huawei-e3372.build {
apn = "data.uk";
username = "user";
password = "one2one";
authType = "chap";
};
profile.gateway = {
lan = {
interfaces = with config.hardware.networkInterfaces;
[
# EDIT: these are the interfaces exposed by the gl.inet gl-ar750:
# if your device has more or differently named lan interfaces,
# specify them here
wlan wlan5
lan
];
inherit (rsecrets.lan) prefix;
address = {
family = "inet"; address ="${rsecrets.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 =
let
secret = outputRef config.services.secrets;
username = secret "ppp/username";
password = secret "ppp/password";
in {
interface =
let
pppoe = svc.pppoe.build {
interface = config.hardware.networkInterfaces.wan;
debug = true;
inherit username password;
};
l2tp =
let
check-address = oneshot rec {
name = "check-lns-address";
up = "grep -Fx ${lns.address} $(output_path ${services.lns-address} addresses)";
dependencies = [ services.lns-address ];
};
route = svc.network.route.build {
via = "$(output ${services.bootstrap-dhcpc} router)";
target = lns.address;
dependencies = [services.bootstrap-dhcpc check-address];
};
l2tpd= svc.l2tp.build {
lns = lns.address;
inherit username password;
dependencies = [config.services.lns-address route check-address];
};
in
svc.health-check.build {
service = l2tpd;
threshold = 3;
interval = 2;
healthCheck = pkgs.writeAshScript "ping-check" {} "ping 1.1.1.1";
};
in svc.round-robin.build {
name = "wan";
services = [
pppoe
l2tp
];
};
dhcp6.enable = true;
};
wireless.networks = {
"${rsecrets.ssid}" = {
interface = config.hardware.networkInterfaces.wlan;
hw_mode = "g";
channel = "6";
ieee80211n = 1;
} // wirelessConfig // {
wpa_passphrase = outputRef config.services.secrets "wpa_passphrase";
};
"${rsecrets.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 // {
wpa_passphrase = outputRef config.services.secrets "wpa_passphrase";
};
};
};
services.bootstrap-dhcpc = svc.network.dhcp.client.build {
interface = config.services.wwan;
dependencies = [ config.services.hostname ];
};
services.sshd = svc.ssh.build {
authorizedKeys = outputRef config.services.secrets "ssh/authorizedKeys";
};
services.lns-address = let
ns = "$(output_word ${services.bootstrap-dhcpc} dns 1)";
route-to-bootstrap-nameserver = svc.network.route.build {
via = "$(output ${services.bootstrap-dhcpc} router)";
target = ns;
dependencies = [services.bootstrap-dhcpc];
};
in oneshot rec {
name = "resolve-l2tp-server";
dependencies = [ services.bootstrap-dhcpc route-to-bootstrap-nameserver ];
up = ''
(in_outputs ${name}
DNSCACHEIP="${ns}" ${pkgs.s6-dns}/bin/s6-dnsip4 ${lns.hostname} \
> addresses
)
'';
};
users.root = rsecrets.root;
programs.busybox.options = {
FEATURE_FANCY_TAIL = "y";
};
}
+19
View File
@@ -0,0 +1,19 @@
{
"wpa_passphrase": "you bring light in",
"ssid": "liminix",
"l2tp": {
"name": "abcde@a.1",
"password": "NotMyIspPassword"
},
"root": {
"passwd": "$6$6pt0mpbgcB7kC2RJ$kSBoCYGyi1.qxt7dqmexLj1l8E6oTZJZmfGyJSsMYMW.jlsETxdgQSdv6ptOYDM7DHAwf6vLG0pz3UD31XBfC1",
"openssh": {
"authorizedKeys": {
"keys": [ ]
}
}
},
"lan": {
"prefix": "10.8.0"
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ in
};
};
config.system.service.bridge = {
primary = liminix.callService ./primary.nix {
primary = config.system.callService ./primary.nix {
ifname = mkOption {
type = types.str;
description = "bridge interface name to create";
-31
View File
@@ -1,31 +0,0 @@
{ config, pkgs, lib, ... }:
let
inherit (pkgs) liminix;
inherit (lib) mkOption types;
in {
imports = [
../service-trigger
];
options = {
system.service.wwan = mkOption {
type = liminix.lib.types.serviceDefn;
};
};
config = {
kernel.config = {
USB_NET_HUAWEI_CDC_NCM = "y";
USB_USBNET = "y";
USB_SERIAL = "y";
USB_SERIAL_OPTION = "y";
};
# https://www.0xf8.org/2017/01/flashing-a-huawei-e3372h-4g-lte-stick-from-hilink-to-stick-mode/
system.service.wwan = config.system.callService ./wwan.nix {
apn = mkOption { type = types.str; };
username = mkOption { type = types.str; };
password = mkOption { type = types.str; };
authType = mkOption { type = types.enum [ "pap" "chap" ]; };
};
};
}
+1 -1
View File
@@ -27,6 +27,6 @@
dir (svc.open state-directory)]
(accumulate [addresses []
v (dir:events)]
(update-prefixes lan-device addresses (v:output "prefix") system))))
(update-prefixes lan-device addresses (or (v:output "prefix") []) system))))
{ : changes : run }
+1 -1
View File
@@ -27,6 +27,6 @@
dir (svc.open state-directory)]
(accumulate [addresses []
v (dir:events)]
(update-addresses wan-device addresses (v:output "address") system))))
(update-addresses wan-device addresses (or (v:output "address") []) system))))
{ : update-addresses : deletions : run }
-1
View File
@@ -3,7 +3,6 @@
, linotify
, anoia
, lualinux
, lua
}:
writeFennel "acquire-wan-address" {
packages = [ linotify anoia lualinux ];
+3 -3
View File
@@ -23,13 +23,13 @@ in
};
};
config.system.service.dhcp6c = {
client = liminix.callService ./client.nix {
client = config.system.callService ./client.nix {
interface = mkOption {
type = liminix.lib.types.interface;
description = "interface (usually WAN) to query for DHCP6";
};
};
address = liminix.callService ./address.nix {
address = config.system.callService ./address.nix {
client = mkOption {
type = types.anything; # liminix.lib.types.service;
};
@@ -38,7 +38,7 @@ in
description = "interface to assign the address to";
};
};
prefix = liminix.callService ./prefix.nix {
prefix = config.system.callService ./prefix.nix {
client = mkOption {
type = types.anything; # liminix.lib.types.service;
};
+1 -1
View File
@@ -16,7 +16,7 @@ in {
};
};
config = {
system.service.dnsmasq = liminix.callService ./service.nix {
system.service.dnsmasq = config.system.callService ./service.nix {
user = mkOption {
type = types.str;
default = "dnsmasq";
-1
View File
@@ -26,7 +26,6 @@ longrun {
inherit name;
dependencies = [ interface ];
run = ''
. ${serviceFns}
${dnsmasq}/bin/dnsmasq \
--user=${user} \
--domain=${domain} \
+2 -2
View File
@@ -90,7 +90,7 @@ in {
# accept inbound from the WAN
(if allow-incoming
then accept "oifname \"int\" iifname \"ppp0\""
else "oifname \"int\" iifname \"ppp0\" jump incoming-allowed-ip6"
else "iifname \"ppp0\" jump incoming-allowed-ip6"
)
# allow all outbound and any inbound that's part of a
# recognised (outbound-initiated) flow
@@ -210,7 +210,7 @@ in {
"icmp type { echo-request, echo-reply } accept"
"iifname int jump input-ip4-lan"
"iifname ppp0 jump input-ip4-wan"
"oifname \"int\" iifname \"ppp0\" jump incoming-allowed-ip4"
"iifname ppp0 jump incoming-allowed-ip4"
"ct state established,related accept"
"log prefix \"DENIED CHAIN=input-ip4 \""
];
+1 -1
View File
@@ -54,7 +54,7 @@ in
};
config = {
system.service.firewall =
let svc = liminix.callService ./service.nix {
let svc = config.system.callService ./service.nix {
extraRules = mkOption {
type = types.attrsOf types.attrs;
description = "firewall ruleset";
+43
View File
@@ -0,0 +1,43 @@
## Health check
##
## Runs a service and a separate periodic health process. When the
## health check starts failing over a period of time, kill the service.
## (Usually that means the supervisor will restart it, but you can
## have other behaviours by e.g. combining this service with a round-robin
## for failover)
{ lib, pkgs, config, ...}:
let
inherit (lib) mkOption types;
inherit (pkgs) liminix;
# inherit (pkgs.liminix.services) longrun;
in {
options = {
system.service.health-check = mkOption {
description = "run a service while periodically checking it is healthy";
type = liminix.lib.types.serviceDefn;
};
};
config.system.service.health-check = config.system.callService ./service.nix {
service = mkOption {
type = liminix.lib.types.service;
};
interval = mkOption {
description = "interval between checks, in seconds";
type = types.int;
default = 10;
example = 10;
};
threshold = mkOption {
description = "number of consecutive failures required for the service to be kicked";
type = types.int;
example = 3;
};
healthCheck = mkOption {
description = "health check command or script. Expected to exit 0 if the service is healthy or any other exit status otherwise";
type = types.path;
};
};
config.programs.busybox.applets = ["expr"];
}
+37
View File
@@ -0,0 +1,37 @@
{
liminix, lib, lim, s6
}:
{ service, interval, threshold, healthCheck } :
let
inherit (liminix.services) oneshot longrun;
inherit (builtins) toString;
inherit (service) name;
checker = let name' = "check-${name}"; in longrun {
name = name';
run = ''
fails=0
echo waiting for /run/service/${name}
${s6}/bin/s6-svwait -U /run/service/${name} || exit
while sleep ${toString interval} ; do
${healthCheck}
if test $? -gt 0; then
fails=$(expr $fails + 1)
else
fails=0
fi
echo fails $fails/${toString threshold} for ${name}
if test "$fails" -gt "${toString threshold}" ; then
echo time to die
${s6}/bin/s6-svc -r /run/service/${name}
echo bounced
fails=0
echo waiting for /run/service/${name}
${s6}/bin/s6-svwait -U /run/service/${name}
fi
done
'';
};
in service.overrideAttrs(o: {
buildInputs = (lim.orEmpty o.buildInputs) ++ [ checker ];
dependencies = (lim.orEmpty o.dependencies) ++ [ checker ];
})
+2 -1
View File
@@ -16,13 +16,14 @@ let
inherit (lib) mkOption types;
inherit (pkgs) liminix;
in {
imports = [ ../secrets ];
options = {
system.service.hostapd = mkOption {
type = liminix.lib.types.serviceDefn;
};
};
config = {
system.service.hostapd = liminix.callService ./service.nix {
system.service.hostapd = config.system.callService ./service.nix {
interface = mkOption {
type = liminix.lib.types.service;
};
+32 -12
View File
@@ -1,13 +1,16 @@
{
liminix
, svc
, hostapd
, output-template
, writeText
, lib
}:
{ interface, params} :
let
inherit (liminix.services) longrun;
inherit (lib) concatStringsSep mapAttrsToList;
inherit (lib) concatStringsSep mapAttrsToList unique ;
inherit (builtins) map filter attrValues length head typeOf;
# This is not a friendly interface to configuring a wireless AP: it
# just passes everything straight through to the hostapd config.
@@ -20,18 +23,35 @@ let
driver = "nl80211";
logger_syslog = "-1";
logger_syslog_level = 1;
ctrl_interface = "/run/hostapd";
ctrl_interface = "/run/${name}";
ctrl_interface_group = 0;
};
attrs = defaults // params ;
literal_or_output = o: ({
string = builtins.toJSON;
int = builtins.toJSON;
lambda = (o: "output(${builtins.toJSON (o "service")}, ${builtins.toJSON (o "path")})");
}.${builtins.typeOf o}) o;
conf = writeText "hostapd.conf"
(concatStringsSep
"\n"
(mapAttrsToList
(name: value: "${name}=${toString value}")
(defaults // params)));
in longrun {
inherit name;
dependencies = [ interface ];
run = "${hostapd}/bin/hostapd -i $(output ${interface} ifname) -P /run/${name}.pid -S ${conf}";
conf =
(writeText "hostapd.conf.in"
((concatStringsSep
"\n"
(mapAttrsToList
(n : v : "${n}={{ ${literal_or_output v} }}")
attrs)) + "\n"));
service = longrun {
inherit name;
dependencies = [ interface ];
run = ''
mkdir -p /run/${name}
chmod 0700 /run/${name}
${output-template}/bin/output-template '{{' '}}' < ${conf} > /run/${name}/hostapd.conf
exec ${hostapd}/bin/hostapd -i $(output ${interface} ifname) -P /run/${name}/hostapd.pid -S /run/${name}/hostapd.conf
'';
};
watch = filter (f: typeOf f == "lambda") (attrValues attrs);
in svc.secrets.subscriber.build {
inherit service watch;
action = "restart-all";
}
+1 -2
View File
@@ -91,9 +91,8 @@ in {
IKCONFIG_PROC = "y";
PROC_FS = "y";
KEXEC = "y";
MODULES = if modular then "y" else "n";
MODULE_SIG = if modular then "y" else "n";
MODULE_UNLOAD = if modular then "y" else "n";
DEBUG_FS = "y";
# basic networking protocols
+15
View File
@@ -0,0 +1,15 @@
{ config, lib, pkgs, ... }:
let
inherit (pkgs.liminix.services) longrun;
in {
config.services.klogd = longrun {
name = "klogd";
run = ''
echo "1 2 1 8" > /proc/sys/kernel/printk
cat /proc/kmsg
'';
finish = ''
echo "8 4 1 8" > /proc/sys/kernel/printk
'';
};
}
+2
View File
@@ -11,7 +11,9 @@ in {
devout = longrun {
name = "devout";
notification-fd = 10;
timeout-up = 60 * 1000;
run = "exec ${pkgs.devout}/bin/devout /run/devout.sock 4";
dependencies = [ mdevd ];
};
coldplug = oneshot {
name = "coldplug";
+1 -1
View File
@@ -14,7 +14,7 @@ in {
type = liminix.lib.types.serviceDefn;
};
};
imports = [ ../mdevd.nix ../service-trigger ];
imports = [ ../mdevd.nix ../uevent-rule ];
config.system.service.mount =
let svc = config.system.callService ./service.nix {
partlabel = mkOption {
-1
View File
@@ -11,7 +11,6 @@ let
# prefixes, or the same but different protocols
name = "${interface.name}.a.${address}";
up = ''
. ${serviceFns}
dev=$(output ${interface} ifname)
ip address add ${address}/${toString prefixLength} dev $dev
(in_outputs ${name}
+5 -5
View File
@@ -64,7 +64,7 @@ in {
services.loopback = config.hardware.networkInterfaces.lo;
system.service.network = {
link = liminix.callService ./link.nix {
link = config.system.callService ./link.nix {
ifname = mkOption {
type = types.str;
example = "eth0";
@@ -89,7 +89,7 @@ in {
example = 1480;
};
};
address = liminix.callService ./address.nix {
address = config.system.callService ./address.nix {
interface = mkOption {
type = liminix.lib.types.service;
};
@@ -104,7 +104,7 @@ in {
};
};
route = liminix.callService ./route.nix {
route = config.system.callService ./route.nix {
interface = mkOption {
type = types.nullOr liminix.lib.types.interface;
default = null;
@@ -125,7 +125,7 @@ in {
};
};
forward = liminix.callService ./forward.nix {
forward = config.system.callService ./forward.nix {
enableIPv4 = mkOption {
type = types.bool;
default = true;
@@ -136,7 +136,7 @@ in {
};
};
dhcp.client = liminix.callService ./dhcpc.nix {
dhcp.client = config.system.callService ./dhcpc.nix {
interface = mkOption {
type = liminix.lib.types.service;
};
+1 -1
View File
@@ -18,7 +18,7 @@ in {
};
};
config = {
system.service.ntp = liminix.callService ./service.nix {
system.service.ntp = config.system.callService ./service.nix {
user = mkOption {
type = types.str;
default = "ntp";
+6 -1
View File
@@ -6,6 +6,7 @@
}:
params:
let
name = "ntp"; # bad name, needs to be unique
inherit (liminix.services) longrun;
inherit (lib) concatStringsSep mapAttrsToList;
configFile = p:
@@ -23,11 +24,15 @@ let
++ (lib.optional (p.bindaddress != null) "bindaddress ${p.bindaddress}")
++ (lib.optional (p.binddevice != null) "binddevice ${p.binddevice}")
++ (lib.optional (p.dumpdir != null) "dumpdir ${p.dumpdir}")
++ [
"bindcmdaddress /" # disable unix socket
"pidfile /run/${name}.pid"
]
++ [p.extraConfig];
config = writeText "chrony.conf"
(concatStringsSep "\n" (configFile params));
in longrun {
name = "ntp"; # bad name, needs to be unique
inherit name;
run = "${chrony}/bin/chronyd -f ${config} -d";
}
+3
View File
@@ -12,6 +12,9 @@ in {
kexecboot = mkOption {
type = types.package;
description = ''
kexecboot
*********
Directory containing files needed for kexec booting.
Can be copied onto the target device using ssh or similar
'';
+92
View File
@@ -0,0 +1,92 @@
{ writeAshScript, liminix, svc, lib, serviceFns, output-template }:
{
command,
name,
debug
, username,
password,
lcpEcho,
ppp-options,
dependencies ? []
} :
let
inherit (lib) optional optionals escapeShellArgs concatStringsSep;
inherit (liminix.services) longrun;
inherit (builtins) toJSON toString typeOf;
ip-up = writeAshScript "ip-up" {} ''
exec >&5 2>&5
. ${serviceFns}
in_outputs ${name}
echo $1 > ifname
echo $2 > tty
echo $3 > speed
echo $4 > address
echo $5 > peer-address
set +o nounset
set +o errexit
if test -n "''${DNS1}" ;then echo ''${DNS1} > ns1 ; fi
if test -n "''${DNS2}" ;then echo ''${DNS2} > ns2 ; fi
set +o pipefail
ls -l /proc/self/fd
echo >/proc/self/fd/10
'';
ip6-up = writeAshScript "ip6-up" {} ''
exec >&5 2>&5
. ${serviceFns}
in_outputs ${name}
set +o nounset
set +o errexit
echo $4 > ipv6-address
echo $5 > ipv6-peer-address
'';
literal_or_output =
let v = o: ({
string = toJSON;
int = toJSON;
lambda = (o: "output(${toJSON (o "service")}, ${toJSON (o "path")})");
}.${typeOf o}) o;
in o: "{{ ${v o} }}";
ppp-options' =
["+ipv6" "noauth"]
++ optional debug "debug"
++ optionals (username != null) ["name" (literal_or_output username)]
++ optionals (password != null) ["password" (literal_or_output password)]
++ optional lcpEcho.adaptive "lcp-echo-adaptive"
++ optionals (lcpEcho.interval != null)
["lcp-echo-interval" (toString lcpEcho.interval)]
++ optionals (lcpEcho.failure != null)
["lcp-echo-failure" (toString lcpEcho.failure)]
++ ppp-options
++ ["ip-up-script" ip-up
"ipv6-up-script" ip6-up
"ipparam" name
"nodetach"
# usepeerdns requests DNS servers from peer (which is good),
# then attempts to write them to /nix/store/xxxx/ppp/resolv.conf
# which causes an unsightly but inconsequential error message
"usepeerdns"
"nodefaultroute"
"logfd" "2"
];
service = longrun {
inherit name;
run = ''
mkdir -p /run/${name}
chmod 0700 /run/${name}
in_outputs ${name}
echo ${escapeShellArgs ppp-options'} | ${output-template}/bin/output-template '{{' '}}' > /run/${name}/ppp-options
exec fdmove -c 5 2 \
${command}
'';
notification-fd = 10;
timeout-up = if lcpEcho.failure != null
then (10 + lcpEcho.failure * lcpEcho.interval) * 1000
else 60 * 1000;
inherit dependencies;
};
in svc.secrets.subscriber.build {
watch = lib.filter (n: typeOf n=="lambda") [ username password ];
inherit service;
}
+84 -5
View File
@@ -1,18 +1,31 @@
## PPP
## ===
##
## A PPPoE (PPP over Ethernet) configuration to address the case where
## your Liminix device is connected to an upstream network using
## ``ppoe`` (PPP over Ethernet) provides a service to address the case
## where your Liminix device is connected to an upstream network using
## PPPoE. This is typical for UK broadband connections where the
## physical connection is made by OpenReach ("Fibre To The X") and
## common in some other localities as well: ask your ISP if this is
## common in some other localities as well: check with your ISP if this is
## you.
##
## ``l2tp`` (Layer 2 Tunelling Protocol) provides a service that
## tunnels PPP over the Internet. This may be used by some ISPs in
## conjunction with a DHCP uplink, or other more creative forms of
## network connection
{ lib, pkgs, config, ...}:
let
inherit (lib) mkOption types;
inherit (pkgs) liminix;
mkStringOption =
description: mkOption {
type = types.nullOr types.str;
default = null;
inherit description;
};
in {
imports = [ ../secrets ];
options = {
system.service.pppoe = mkOption {
type = liminix.lib.types.serviceDefn;
@@ -22,23 +35,89 @@ in {
};
};
config = {
system.service.pppoe = pkgs.liminix.callService ./pppoe.nix {
system.service.pppoe = config.system.callService ./pppoe.nix {
interface = mkOption {
type = liminix.lib.types.service;
description = "ethernet interface to run PPPoE over";
};
username = mkOption {
type = types.nullOr (liminix.lib.types.replacable types.str);
default = null;
description = "username";
};
password = mkOption {
type = types.nullOr (liminix.lib.types.replacable types.str);
default = null;
description = "password";
};
lcpEcho = {
adaptive = mkOption {
description = "send LCP echo-request frames only if no traffic was received from the peer since the last echo-request was sent";
type = types.bool;
default = true;
};
interval = mkOption {
type = types.nullOr types.int;
default = 3;
description = "send an LCP echo-request frame to the peer every n seconds";
};
failure = mkOption {
type = types.nullOr types.int;
default = 3;
description = "terminate connection if n LCP echo-requests are sent without receiving a valid LCP echo-reply";
};
};
debug = mkOption {
description = "log the contents of all control packets sent or received";
default = false;
type = types.bool;
};
ppp-options = mkOption {
type = types.listOf types.str;
description = "options supplied on ppp command line";
default = [];
};
};
system.service.l2tp = pkgs.liminix.callService ./l2tp.nix {
system.service.l2tp = config.system.callService ./l2tp.nix {
lns = mkOption {
type = types.str;
description = "hostname or address of the L2TP network server";
};
username = mkOption {
type = types.nullOr (liminix.lib.types.replacable types.str);
default = null;
description = "username";
};
password = mkOption {
type = types.nullOr (liminix.lib.types.replacable types.str);
default = null;
description = "password";
};
lcpEcho = {
adaptive = mkOption {
description = "send LCP echo-request frames only if no traffic was received from the peer since the last echo-request was sent";
type = types.bool;
default = true;
};
interval = mkOption {
type = types.nullOr types.int;
default = 3;
description = "send an LCP echo-request frame to the peer every n seconds";
};
failure = mkOption {
type = types.nullOr types.int;
default = 3;
description = "terminate connection if n LCP echo-requests are sent without receiving a valid LCP echo-reply";
};
};
debug = mkOption {
description = "log the contents of all control packets sent or received";
default = false;
type = types.bool;
};
ppp-options = mkOption {
type = types.listOf types.str;
default = [];
description = "options supplied on ppp command line";
};
};
+26 -45
View File
@@ -1,59 +1,40 @@
{
liminix
, writeAshScript
, writeText
, serviceFns
, xl2tpd
lib,
liminix,
output-template,
serviceFns,
svc,
writeAshScript,
writeText,
xl2tpd,
callPackage
} :
{ lns, ppp-options }:
{ lns,
ppp-options,
lcpEcho,
username,
password,
debug
}:
let
inherit (liminix.services) longrun;
name = "${lns}.l2tp";
ip-up = writeAshScript "ip-up" {} ''
. ${serviceFns}
(in_outputs ${name}
echo $1 > ifname
echo $2 > tty
echo $3 > speed
echo $4 > address
echo $5 > peer-address
echo $DNS1 > ns1
echo $DNS2 > ns2
)
echo >/proc/self/fd/10
'';
ip6-up = writeAshScript "ip6-up" {} ''
. ${serviceFns}
(in_outputs ${name}
echo $4 > ipv6-address
echo $5 > ipv6-peer-address
)
echo >/proc/self/fd/10
'';
ppp-options' = ppp-options ++ [
"ip-up-script" ip-up
"ipv6-up-script" ip6-up
"ipparam" name
"nodetach"
"usepeerdns"
"logfd" "2"
];
common = callPackage ./common.nix { inherit svc; };
conf = writeText "xl2tpd.conf" ''
[lac upstream]
lns = ${lns}
require authentication = no
pppoptfile = ${writeText "ppp-options" ppp-options'}
pppoptfile = /run/${name}/ppp-options
autodial = yes
redial = yes
redial timeout = 1
max redials = 2 # this gives 1 actual retry, as xl2tpd can't count
'';
control = "/run/xl2tpd/control-${name}";
in
longrun {
inherit name;
run = ''
mkdir -p /run/xl2tpd
control = "/run/${name}/control";
in common {
inherit name debug username password lcpEcho ppp-options;
command = ''
touch ${control}
exec ${xl2tpd}/bin/xl2tpd -D -p /run/xl2tpd/${name}.pid -c ${conf} -C ${control}
exec ${xl2tpd}/bin/xl2tpd -D -p /run/${name}/${name}.pid -c ${conf} -C ${control}
'';
notification-fd = 10;
}
+23 -44
View File
@@ -1,51 +1,30 @@
{
liminix
, lib
, ppp
, pppoe
, writeAshScript
, serviceFns
lib,
liminix,
output-template,
ppp,
pppoe,
serviceFns,
svc,
writeAshScript,
callPackage
} :
{ interface, ppp-options }:
{ interface,
ppp-options,
lcpEcho,
username,
password,
debug
}:
let
inherit (liminix.services) longrun;
name = "${interface.name}.pppoe";
ip-up = writeAshScript "ip-up" {} ''
. ${serviceFns}
(in_outputs ${name}
echo $1 > ifname
echo $2 > tty
echo $3 > speed
echo $4 > address
echo $5 > peer-address
echo $DNS1 > ns1
echo $DNS2 > ns2
)
echo >/proc/self/fd/10
common = callPackage ./common.nix { inherit svc; };
timeoutOpt = if lcpEcho.interval != null then "-T ${builtins.toString (4 * lcpEcho.interval)}" else "";
in common {
inherit name debug username password lcpEcho ppp-options;
command = ''
${ppp}/bin/pppd pty "${pppoe}/bin/pppoe ${timeoutOpt} -I $(output ${interface} ifname)" file /run/${name}/ppp-options
'';
ip6-up = writeAshScript "ip6-up" {} ''
. ${serviceFns}
(in_outputs ${name}
echo $4 > ipv6-address
echo $5 > ipv6-peer-address
)
echo >/proc/self/fd/10
'';
ppp-options' = ppp-options ++ [
"ip-up-script" ip-up
"ipv6-up-script" ip6-up
"ipparam" name
"nodetach"
"usepeerdns"
"logfd" "2"
];
in
longrun {
inherit name;
run = ''
. ${serviceFns}
${ppp}/bin/pppd pty "${pppoe}/bin/pppoe -I $(output ${interface} ifname)" ${lib.concatStringsSep " " ppp-options'}
'';
notification-fd = 10;
dependencies = [ interface ];
}
+1 -11
View File
@@ -52,8 +52,6 @@ in {
wan = {
interface = mkOption { type = liminix.lib.types.interface; };
username = mkOption { type = types.str; };
password = mkOption { type = types.str; };
dhcp6.enable = mkOption { type = types.bool; };
};
@@ -86,14 +84,7 @@ in {
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.wan = cfg.wan.interface;
services.packet_forwarding = svc.network.forward.build { };
@@ -158,7 +149,6 @@ in {
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
-1
View File
@@ -81,7 +81,6 @@ in {
# '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
+27
View File
@@ -0,0 +1,27 @@
## Round Robin
##
## Given a list of services, run each in turn until it exits, then
## runs the next.
{ lib, pkgs, config, ...}:
let
inherit (lib) mkOption types;
inherit (pkgs) liminix;
inherit (pkgs.liminix.services) longrun;
in {
options = {
system.service.round-robin = mkOption {
description = "run services one at a time and failover to next";
type = liminix.lib.types.serviceDefn;
};
};
config.system.service.round-robin = config.system.callService ./service.nix {
services = mkOption {
type = types.listOf liminix.lib.types.service;
};
name = mkOption {
type = types.str;
};
};
}
+32
View File
@@ -0,0 +1,32 @@
{
liminix, lib, s6-rc-round-robin
}:
{ services, name} :
let
inherit (liminix.services) oneshot longrun;
controlled-services = builtins.map
(s: s.overrideAttrs(o: { inherit controller; }))
services;
controller = let name' = "control-${name}"; in longrun {
name = name';
run = ''
in_outputs ${name'}
exec ${s6-rc-round-robin}/bin/s6-rc-round-robin \
-p ${proxy.name} \
${lib.concatStringsSep " "
(builtins.map (f: f.name) controlled-services)}
'';
};
proxy = oneshot rec {
inherit name;
inherit controller;
buildInputs = controlled-services;
up = ''
echo start proxy ${name}
set -x
(in_outputs ${name}
cp -rv $(output_path ${controller} active)/* .
)
'';
};
in proxy
+79 -8
View File
@@ -1,4 +1,4 @@
{ config, pkgs, lib, ... }:
{ config, pkgs, lib, lim, ... }:
let
inherit (pkgs)
execline
@@ -8,8 +8,9 @@ let
stdenvNoCC;
inherit (lib.lists) unique concatMap;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs.liminix.services) oneshot bundle;
inherit (pkgs.liminix.services) oneshot bundle longrun;
inherit (lib) mkIf mkEnableOption mkOption types;
cfg = config.logging;
s6-rc-db =
let
# In the default bundle we need to have all the services
@@ -106,11 +107,22 @@ let
};
notification-fd = { file = "3"; };
run = {
file = ''
#!${execline}/bin/execlineb -P
${execline}/bin/redirfd -w 1 /dev/null
${execline}/bin/redirfd -rnb 0 fifo
${s6}/bin/s6-log -bpd3 -- t /run/uncaught-logs
file =
let pstore = if cfg.pstore then "pipeline { tee /dev/pmsg0 }" else "";
in ''
#!${execline}/bin/execlineb -P
${execline}/bin/redirfd -w 1 /dev/null
${execline}/bin/redirfd -rnb 0 fifo
${if cfg.shipping.enable then ''
pipeline { ${s6}/bin/s6-log -bpd3 -- ${cfg.script} 1 }
pipeline { ${pkgs.logshipper}/bin/logtap ${cfg.shipping.socket} logshipper-socket-event }
${pstore}
${s6}/bin/s6-log -- ${cfg.directory}
'' else ''
pipeline { ${s6}/bin/s6-log -bpd3 -- ${cfg.script} 1 }
${pstore}
${s6}/bin/s6-log -- ${cfg.directory}
''}
'';
mode = "0755";
};
@@ -203,7 +215,66 @@ let
};
};
in {
options = {
logging = {
pstore = mkEnableOption "system logs in pstore for retention after reboot";
shipping = {
enable = mkEnableOption "unix socket for log shipping";
socket = mkOption {
description = "socket pathname"; type = types.path;
default = "/run/.log-shipping.sock";
};
service = mkOption {
description = "log shipper service";
type = pkgs.liminix.lib.types.service;
};
};
script = mkOption {
description = "\"log script\" used by fallback s6-log process";
type = types.str;
default = "p${config.hostname} t";
};
directory = mkOption {
description = "default log directory";
default = "/run/log";
type = types.path;
};
};
};
imports = [
( {config, pkgs, lib, ...}:
let
cfg = config.logging;
pipeline = shipper: bundle {
name = "log-shipping-pipe";
contents = let
eat = longrun {
name = "log-shipping-pipe-eat";
run = ''
fdmove -c 12 1 \
${pkgs.s6}/bin/s6-ipcserver ${cfg.shipping.socket} \
fdmove -c 1 12 \
cat
'';
producer-for = spew.name;
};
spew = shipper.override {
consumer-for ="log-shipping-pipe-eat";
};
in [ eat spew ];
};
in mkIf cfg.shipping.enable {
services.${cfg.shipping.service.name} = pipeline cfg.shipping.service;
}
)];
config = {
kernel.config = mkIf cfg.pstore {
PSTORE = "y";
PSTORE_PMSG = "y";
};
programs.busybox.applets = mkIf cfg.pstore [ "tee" ];
filesystem = dir {
etc = dir {
s6-rc = dir {
+86
View File
@@ -0,0 +1,86 @@
## Secrets
## various ways to manage secrets without writing them to the
## nix store
{ lib, pkgs, config, ...}:
let
inherit (lib) mkOption types;
inherit (pkgs) liminix;
inherit (pkgs.liminix.services) longrun;
in {
options.system.service.secrets = {
outboard = mkOption {
description = "fetch secrets from external vault with https";
type = liminix.lib.types.serviceDefn;
};
tang = mkOption {
description = "fetch secrets from encrypted local pathname, using tang";
type = liminix.lib.types.serviceDefn;
};
subscriber = mkOption {
description = "wrapper around a service that needs notifying (e.g. restarting) when secrets change";
type = liminix.lib.types.serviceDefn;
};
};
config.system.service.secrets = {
outboard = config.system.callService ./outboard.nix {
url = mkOption {
description = "source url";
type = types.strMatching "https?://.*";
};
username = mkOption {
description = "username for HTTP basic auth";
type = types.nullOr types.str;
};
password = mkOption {
description = "password for HTTP basic auth";
type = types.nullOr types.str;
};
name = mkOption {
description = "service name";
type = types.str;
};
interval = mkOption {
type = types.int;
default = 30;
description = "how often to check the source, in minutes";
};
};
tang = config.system.callService ./tang.nix {
path = mkOption {
description = "encrypted source pathname";
type = types.path;
};
name = mkOption {
description = "service name";
type = types.str;
};
interval = mkOption {
type = types.int;
default = 30;
description = "how often to check the source, in minutes";
};
};
subscriber = config.system.callService ./subscriber.nix {
watch = mkOption {
description = "secrets paths to subscribe to";
type = types.listOf (types.functionTo types.anything);
};
service = mkOption {
description = "subscribing service that will receive notification";
type = liminix.lib.types.service;
};
action = mkOption {
description = "how do we notify the service to regenerate its config";
default = "restart-all";
type = types.enum [
"restart" "restart-all"
"hup" "int" "quit" "kill" "term"
"winch" "usr1" "usr2"
];
};
};
};
}
+23
View File
@@ -0,0 +1,23 @@
{
liminix, lib, json-to-fstree, serviceFns
}:
{ name, url, interval, username, password } :
let
inherit (liminix.services) oneshot longrun;
inherit (lib) optionalString;
in longrun {
inherit name;
buildInputs = [ json-to-fstree ];
run = ''
${optionalString (username != null) ''
export NETRC=$(mkstate ${name})/netrc
(echo default ; echo login ${username} ; echo password ${password} ) > $NETRC
''}
( in_outputs ${name}
while : ; do
${json-to-fstree}/bin/json-to-fstree ${url} .
sleep ${builtins.toString (interval * 60)}
done
)
'';
}
+51
View File
@@ -0,0 +1,51 @@
{
liminix, lib, lim, s6, s6-rc, watch-outputs
}:
{ watch, service, action } :
let
inherit (liminix.services) oneshot longrun;
inherit (builtins) length head toString;
inherit (lib) unique optional optionals;
inherit (service) name;
watched-services = unique (map (f: f "service") watch);
paths = unique (map (f: f "path") watch);
restart-flag = {
restart = "-r";
restart-all = "-R";
"hup" = "-s 1";
"int" = "-s 2";
"quit" = "-s 3";
"kill" = "-s 9";
"term" = "-s 15";
"winch" = "-s 28";
"usr1" = "-s 10";
"usr2" = "-s 12";
}.${action};
watched-service =
if length watched-services == 0
then null
else if length watched-services == 1
then head watched-services
else throw "cannot subscribe to more than one source service for secrets";
watcher = let name' = "restart-${name}"; in longrun {
name = name';
run = ''
dir=/run/service/${name}
echo waiting for $dir
if test -e $dir/notification-fd; then flag="-U"; else flag="-u"; fi
${s6}/bin/s6-svwait $flag /run/service/${name} || exit
PATH=${s6-rc}/bin:${s6}/bin:$PATH
${watch-outputs}/bin/watch-outputs ${restart-flag} ${name} ${watched-service.name} ${lib.concatStringsSep " " paths}
'';
};
in service.overrideAttrs(o: {
buildInputs = (lim.orEmpty o.buildInputs) ++
optional (watched-service != null) watcher;
dependencies = (lim.orEmpty o.dependencies) ++
optionals (watched-service != null)
[ watcher watched-service ];
})
+26
View File
@@ -0,0 +1,26 @@
{
liminix, lib, json-to-fstree, serviceFns, tangc
}:
{ name, path, interval } :
let
inherit (liminix.services) longrun;
inherit (lib) optionalString;
in longrun {
inherit name;
buildInputs = [ json-to-fstree ];
notification-fd = 10;
run = ''
set -e
statedir=/run/${name}
mkdir -p -m 0700 $statedir
( in_outputs ${name}
while : ; do
${tangc}/bin/tangc decrypt < ${path} > $statedir/.input.json
mv $statedir/.input.json $statedir/input.json
${json-to-fstree}/bin/json-to-fstree file://$statedir/input.json .
echo ready >&10
sleep ${builtins.toString (interval * 60)}
done
)
'';
}
+12 -1
View File
@@ -19,8 +19,9 @@ in {
type = liminix.lib.types.serviceDefn;
};
};
config.programs.busybox.options.FEATURE_FANCY_ECHO = "y";
config.system.service = {
ssh = liminix.callService ./ssh.nix {
ssh = config.system.callService ./ssh.nix {
address = mkOption {
type = types.nullOr types.str;
default = null;
@@ -41,6 +42,16 @@ in {
type = types.bool; default = false;
description = "Allow remote hosts to connect to local forwarded ports (by default they are bound to loopback)";
};
authorizedKeys = mkOption {
type = types.nullOr (liminix.lib.types.replacable (types.attrsOf (types.listOf types.nonEmptyStr)));
example = {
root = ["ssh-rsa AAAAB3N...aZaZ"];
alice = ["ssh-rsa AAAAB3N...qS4r"];
bob = [];
};
default = null;
description = "Authorized SSH public keys for each username. If this optin is provided it overrides any keys found in /home/{username}/.ssh";
};
extraConfig = mkOption {
type = types.separatedString " ";
default = "";
+52 -13
View File
@@ -2,11 +2,26 @@
liminix
, dropbear
, lib
, watch-ssh-keys
}:
{
address,
allowLocalPortForward,
allowPasswordLogin,
allowPasswordLoginForRoot,
allowRemoteConnectionToForwardedPorts,
allowRemotePortForward,
allowRoot,
authorizedKeys,
port,
extraConfig
}:
p :
let
name = "sshd";
inherit (builtins) toString typeOf;
inherit (liminix.services) longrun;
inherit (lib) concatStringsSep;
inherit (lib) concatStringsSep mapAttrs mapAttrsToList;
keydir = "/run/${name}/authorized_keys";
options =
[
"-e" # pass environment to child
@@ -15,26 +30,50 @@ let
"-P /run/dropbear.pid"
"-F" # don't fork into background
] ++
(lib.optional (! p.allowRoot) "-w") ++
(lib.optional (! p.allowPasswordLogin) "-s") ++
(lib.optional (! p.allowPasswordLoginForRoot) "-g") ++
(lib.optional (! p.allowLocalPortForward) "-j") ++
(lib.optional (! p.allowRemotePortForward) "-k") ++
(lib.optional (! p.allowRemoteConnectionToForwardedPorts) "-a") ++
[(if p.address != null
then "-p ${p.address}:${p.port}"
else "-p ${builtins.toString p.port}")] ++
[p.extraConfig];
(lib.optional (! allowRoot) "-w") ++
(lib.optional (! allowPasswordLogin) "-s") ++
(lib.optional (! allowPasswordLoginForRoot) "-g") ++
(lib.optional (! allowLocalPortForward) "-j") ++
(lib.optional (! allowRemotePortForward) "-k") ++
(lib.optional (! allowRemoteConnectionToForwardedPorts) "-a") ++
(lib.optionals (authorizedKeys != null) ["-U" "${keydir}/%n"]) ++
[(if address != null
then "-p ${address}:${toString port}"
else "-p ${toString port}")] ++
[extraConfig];
isKeyservice = typeOf authorizedKeys == "lambda";
authKeysConcat =
if authorizedKeys != null && !isKeyservice
then mapAttrs
(n : v : concatStringsSep "\\n" v)
authorizedKeys
else {};
keyservice = longrun {
name = "${name}-watch-keys";
run = ''
mkdir -p ${keydir}
exec ${watch-ssh-keys}/bin/watch-ssh-keys -d ${keydir} ${authorizedKeys "service"} ${authorizedKeys "path"}
'';
dependencies = [ (authorizedKeys "service") ] ;
};
in
longrun {
name = "sshd";
inherit name;
# we need /run/dropbear to point to hostkey storage, as that
# pathname is hardcoded into the binary.
# env -i clears the environment so we don't pass anything weird to
# ssh sessions
run = ''
ln -s $(mkstate dropbear) /run
mkdir -p /run/${name}/authorized_keys
${concatStringsSep "\n"
(mapAttrsToList
(n : v : "echo -e '${v}' > /run/${name}/authorized_keys/${n} ")
authKeysConcat
)
}
. /etc/profile # sets PATH but do we need this? it's the same file as ashrc
exec env -i ENV=/etc/ashrc PATH=$PATH ${dropbear}/bin/dropbear ${concatStringsSep " " options}
'';
dependencies = lib.optional isKeyservice keyservice;
}
@@ -0,0 +1,21 @@
{ liminix, certifix-client, svc, lib, writeText, serviceFns }:
{
caCertificate,
secret,
subject,
serviceUrl
}:
let
inherit (builtins) filter isString split;
inherit (liminix.services) oneshot;
name = "certifix-${lib.strings.sanitizeDerivationName subject}";
caCertFile = writeText "ca.crt" caCertificate;
secretFile = writeText "secret" secret;
in oneshot {
inherit name;
up = ''
(in_outputs ${name}
SSL_CA_CERT_FILE=${caCertFile} ${certifix-client}/bin/certifix-client --subject ${subject} --secret ${secretFile} --key-out key --certificate-out cert ${serviceUrl}
)
'';
}
+40
View File
@@ -0,0 +1,40 @@
{ lib, pkgs, config, ...}:
let
inherit (lib) mkOption types;
inherit (pkgs) liminix;
in
{
options = {
system.service.tls-certificate = {
certifix-client = mkOption {
type = liminix.lib.types.serviceDefn;
};
};
};
config.system.service.tls-certificate.certifix-client =
config.system.callService ./certifix-client.nix {
# this is probably read from files on the build machine,
# but are not named with ...File suffix because they are
# not files on the device (they get embedded into the store)
caCertificate = mkOption {
description = "CA certificate in PEM format. This must be the same CA as that which signed the certificate of the Certifix server";
type = types.str;
};
secret = mkOption {
description = "The shared secret to embed in signing request. This must match the secret configured in the Certifix service, otherwise it will refuse to sign the CSR.";
type = types.str;
};
subject = mkOption {
description = "Subject of the certificate request, as an X509 DN. The CN ('Common Name') you provide here is also used as the value of the SubjectAlternativeName extension.";
type = types.str;
example = "C=GB,ST=London,O=Liminix,OU=IT,CN=myhostname";
};
serviceUrl = mkOption {
description = "Certifix server endpoint URL";
type = types.str;
example = "https://certifix.lan:19613/sign";
};
};
}
@@ -14,7 +14,7 @@ in {
};
};
config = {
system.service.uevent-rule = liminix.callService ./rule.nix {
system.service.uevent-rule = config.system.callService ./rule.nix {
serviceName = mkOption {
description = "name of the service to run when the rule matches";
type = types.str;
+1 -1
View File
@@ -19,7 +19,7 @@ in
options = {
system.service.vlan = mkOption { type = liminix.lib.types.serviceDefn; };
};
config.system.service.vlan = liminix.callService ./service.nix {
config.system.service.vlan = config.system.callService ./service.nix {
ifname = mkOption {
type = types.str;
description = "interface name to create";
+1 -1
View File
@@ -15,7 +15,7 @@ in
type = liminix.lib.types.serviceDefn;
};
};
config.system.service.watchdog = liminix.callService ./watchdog.nix {
config.system.service.watchdog = config.system.callService ./watchdog.nix {
watched = mkOption {
description = "services to watch";
type = types.listOf liminix.lib.types.service;
+49
View File
@@ -0,0 +1,49 @@
{ config, pkgs, lib, ... }:
let
inherit (pkgs) liminix;
inherit (lib) mkOption types;
huawei-cdc-ncm = pkgs.kmodloader.override {
targets = ["huawei_cdc_ncm"];
inherit (config.system.outputs) kernel;
};
in {
imports = [
../uevent-rule
../mdevd.nix
];
options = {
system.service.wwan.huawei-e3372 = mkOption {
type = liminix.lib.types.serviceDefn;
};
};
config = {
kernel.config = {
USB_NET_HUAWEI_CDC_NCM = "m";
USB_USBNET = "y";
USB_SERIAL = "y";
USB_SERIAL_OPTION = "y";
};
programs.busybox.applets = [
"insmod" "rmmod"
];
# https://www.0xf8.org/2017/01/flashing-a-huawei-e3372h-4g-lte-stick-from-hilink-to-stick-mode/
system.service.wwan.huawei-e3372 =
let svc = config.system.callService ./huawei-e3372.nix {
apn = mkOption { type = types.str; };
username = mkOption { type = types.str; };
password = mkOption { type = types.str; };
authType = mkOption { type = types.enum [ "pap" "chap" ]; };
};
in
svc // {
build = args :
let args' = args // {
dependencies = (args.dependencies or []) ++
[huawei-cdc-ncm];
};
in svc.build args' ;
};
};
}
@@ -41,7 +41,10 @@ let
};
atz = oneshot rec {
name = "modem-atz";
dependencies = [ modeswitch ];
# atz does not depend on modeswitch because modeswitch service
# is only running when the stick is in the wrong mode
dependencies = [ modeswitch.controller ];
buildInputs = [ modeswitch ];
controller = (svc.uevent-rule.build {
serviceName = name;
terms = {
+1 -1
View File
@@ -20,7 +20,7 @@ in
type = liminix.lib.types.serviceDefn;
};
config.boot.zyxel-dual-image = liminix.callService ./service.nix {
config.boot.zyxel-dual-image = config.system.callService ./service.nix {
ensureActiveImage = mkOption {
type = types.enum [ "primary" "secondary" ];
default = "primary";
+153 -65
View File
@@ -1,22 +1,27 @@
final: prev:
let
isCross = final.stdenv.buildPlatform != final.stdenv.hostPlatform;
crossOnly = pkg : amendFn : if isCross then (amendFn pkg) else pkg;
extraPkgs = import ./pkgs/default.nix {
inherit (final) lib callPackage;
};
inherit (final) fetchpatch;
lua_no_readline = prev.lua5_3.overrideAttrs(o: {
name = "lua-tty";
preBuild = ''
makeFlagsArray+=(PLAT="posix" SYSLIBS="-Wl,-E -ldl" CFLAGS="-O2 -fPIC -DLUA_USE_POSIX -DLUA_USE_DLOPEN")
luaHost =
let
l = prev.lua5_3.overrideAttrs(o: {
name = "lua-tty";
preBuild = ''
makeFlagsArray+=(PLAT="posix" SYSLIBS="-Wl,-E -ldl" CFLAGS="-O2 -fPIC -DLUA_USE_POSIX -DLUA_USE_DLOPEN")
'';
# lua in nixpkgs has a postInstall stanza that assumes only
# one output, we need to override that if we're going to
# convert to multi-output
# outputs = ["bin" "man" "out"];
makeFlags =
builtins.filter (x: (builtins.match "(PLAT|MYLIBS).*" x) == null)
o.makeFlags;
});
# lua in nixpkgs has a postInstall stanza that assumes only
# one output, we need to override that if we're going to
# convert to multi-output
# outputs = ["bin" "man" "out"];
makeFlags =
builtins.filter (x: (builtins.match "(PLAT|MYLIBS).*" x) == null)
o.makeFlags;
});
in l.override { self = l; };
s6 = prev.s6.overrideAttrs(o:
let
@@ -41,20 +46,22 @@ let
(if o ? patches then o.patches else []) ++
(if patch_needed then [ patch ] else []);
});
lua = let s = lua_no_readline.override { self = s; }; in s;
in
extraPkgs // {
# liminix library functions
lim = {
parseInt = s: (builtins.fromTOML "r=${s}").r;
orEmpty = x: if x != null then x else [];
};
# keep these alphabetical
btrfs-progs = prev.btrfs-progs.override {
udevSupport = false;
udev = null;
};
btrfs-progs = crossOnly prev.btrfs-progs (
d: d.override {
udevSupport = false;
udev = null;
}
);
chrony =
let chrony' = prev.chrony.overrideAttrs(o: {
@@ -76,22 +83,33 @@ extraPkgs // {
texinfo = null;
};
# clevis without luks/tpm
clevis = crossOnly prev.clevis
(d: let c = d.overrideAttrs(o: {
outputs = ["out"];
preConfigure = ''
rm -rf src/luks
sed -i -e '/luks/d' src/meson.build
'';
}); in c.override {
asciidoc = null;
cryptsetup = null;
luksmeta = null;
tpm2-tools = null;
});
# 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 {
cmake = crossOnly prev.cmake
(d: d.overrideAttrs(o: {
preConfigure =
builtins.replaceStrings
["$configureFlags"] ["$configureFlags $cmakeFlags"] o.preConfigure;
}
else {}
);
));
dnsmasq =
let d = prev.dnsmasq.overrideAttrs(o: {
@@ -104,14 +122,39 @@ extraPkgs // {
nettle = null;
};
dropbear = prev.dropbear.overrideAttrs (o: {
postPatch = ''
(echo '#define DSS_PRIV_FILENAME "/run/dropbear/dropbear_dss_host_key"'
echo '#define RSA_PRIV_FILENAME "/run/dropbear/dropbear_rsa_host_key"'
echo '#define ECDSA_PRIV_FILENAME "/run/dropbear/dropbear_ecdsa_host_key"'
echo '#define ED25519_PRIV_FILENAME "/run/dropbear/dropbear_ed25519_host_key"') > localoptions.h
'';
});
dropbear = crossOnly prev.dropbear
(d: d.overrideAttrs (o: rec {
version = "2024.85";
src = final.fetchurl {
url = "https://matt.ucc.asn.au/dropbear/releases/dropbear-${version}.tar.bz2";
sha256 = "sha256-hrA2xDOmnYnOUeuuM11lxHc4zPkNE+XrD+qDLlVtpQI=";
};
patches =
# need to update nixpkgs patch for new version of dropbear
let passPath = final.runCommand "pass-path" {} ''
sed < ${builtins.head o.patches} -e 's,svr-chansession.c,src/svr-chansession.c,g' > $out
'';
in [
passPath
./pkgs/dropbear/add-authkeyfile-option.patch
];
postPatch = ''
(echo '#define DSS_PRIV_FILENAME "/run/dropbear/dropbear_dss_host_key"'
echo '#define RSA_PRIV_FILENAME "/run/dropbear/dropbear_rsa_host_key"'
echo '#define ECDSA_PRIV_FILENAME "/run/dropbear/dropbear_ecdsa_host_key"'
echo '#define ED25519_PRIV_FILENAME "/run/dropbear/dropbear_ed25519_host_key"') > localoptions.h
'';
}));
elfutils = crossOnly prev.elfutils
(d: let e = d.overrideAttrs(o: {
configureFlags = o.configureFlags ++[
"ac_cv_has_stdatomic=no"
];
});
in e.override {
enableDebuginfod = false;
});
hostapd =
let
@@ -141,6 +184,10 @@ extraPkgs // {
});
in h.override { openssl = null; sqlite = null; };
# berlekey db needs libatomic which we haven't figured out yet.
# disabling it means we don't have arpd
iproute2 = crossOnly prev.iproute2 (d: d.override { db = null; });
kexec-tools-static = prev.kexec-tools.overrideAttrs(o: {
# For kexecboot we copy kexec into a ramdisk on the system being
# upgraded from. This is more likely to work if kexec is
@@ -159,8 +206,16 @@ extraPkgs // {
];
});
luaFull = prev.lua;
inherit lua;
lua = crossOnly prev.lua5_3 (_: luaHost);
luaossl' = luaHost.pkgs.luaossl.overrideAttrs(o: {
patches = [
(fetchpatch {
url = "https://patch-diff.githubusercontent.com/raw/wahern/luaossl/pull/218.patch";
hash = "sha256-2GOliY4/RUzOgx3rqee3X3szCdUVxYDut7d+XFcUTJw=";
})
] ++ final.lib.optionals (o ? patches) o.patches;
});
mtdutils = prev.mtdutils.overrideAttrs(o: {
patches = (if o ? patches then o.patches else []) ++ [
@@ -177,30 +232,59 @@ extraPkgs // {
];
});
openssl = prev.openssl.overrideAttrs (o: {
# we want to apply
# https://patch-diff.githubusercontent.com/raw/openssl/openssl/pull/20273.patch";
# which disables overriding the -march cflags to the wrong values,
# but openssl is used for bootstrapping so that's easier said than
# done. Do it the ugly way..
postPatch =
o.postPatch
+ (
with final;
lib.optionalString (
stdenv.buildPlatform != stdenv.hostPlatform
) "\nsed -i.bak 's/linux.*-mips/linux-mops/' Configure\n"
);
});
openssl = crossOnly prev.openssl
(d: d.overrideAttrs (o:
with final; {
# we want to apply
# https://patch-diff.githubusercontent.com/raw/openssl/openssl/pull/20273.patch";
# which disables overriding the -march cflags to the wrong values,
# but openssl is used for bootstrapping so that's easier said than
# done. Do it the ugly way..
postPatch =
o.postPatch
+ ''
sed -i.bak 's/linux.*-mips/linux-mops/' Configure
'';
# openssl with threads requires stdatomic which drags in libgcc
# as a dependency
configureFlags = ["no-threads"] ++ o.configureFlags;
# don't need or want this bash script
postInstall = o.postInstall + "rm $bin/bin/c_rehash\n";
}
));
pppBuild = prev.ppp;
qemuLim = let q = prev.qemu.overrideAttrs (o: {
patches = o.patches ++ [
./pkgs/qemu/arm-image-friendly-load-addr.patch
];
}); in q.override { nixosTestRunner = true; sdlSupport = false; };
qemuLim =
let
inherit (final) lib;
q = prev.qemu.overrideAttrs (o: {
patches = o.patches ++ [
./pkgs/qemu/arm-image-friendly-load-addr.patch
(final.fetchpatch {
url = "https://lore.kernel.org/qemu-devel/20220322154658.1687620-1-raj.khem@gmail.com/raw";
hash = "sha256-jOsGka7xLkJznb9M90v5TsJraXXTAj84lcphcSxjYLU=";
})
];
});
overrides = {
nixosTestRunner = true;
hostCpuTargets = map (f: "${f}-softmmu") [
"arm" "aarch64" "mips" "mipsel"
];
sdlSupport = false;
numaSupport = false;
seccompSupport = false;
usbredirSupport = false;
libiscsiSupport = false;
tpmSupport = false;
uringSupport = false;
capstoneSupport = false;
} // lib.optionalAttrs (lib.versionOlder lib.version "24.10") {
texinfo = null;
};
in q.override overrides;
rsyncSmall =
let
r = prev.rsync.overrideAttrs (o: {
@@ -272,16 +356,16 @@ extraPkgs // {
'';
};
libusb1 =
let u = prev.libusb1.overrideAttrs(o: {
# don't use gcc libatomic because it vastly increases the
# closure size
preConfigure = "sed -i.bak /__atomic_fetch_add_4/c\: configure.ac";
});
in u.override {
enableUdev = final.stdenv.buildPlatform == final.stdenv.hostPlatform;
withDocs = false;
};
libusb1 = crossOnly prev.libusb1 (
d: let u = d.overrideAttrs(o: {
# don't use gcc libatomic because it vastly increases the
# closure size
preConfigure = "sed -i.bak /__atomic_fetch_add_4/c\: configure.ac";
});
in u.override {
enableUdev = false;
withDocs = false;
});
util-linux-small = prev.util-linux.override {
ncursesSupport = false;
@@ -291,4 +375,8 @@ extraPkgs // {
translateManpages = false;
capabilitiesSupport = false;
};
xl2tpd = prev.xl2tpd.overrideAttrs(o: {
patches = [ ./pkgs/xl2tpd-exit-on-close.patch ];
});
}
+15 -4
View File
@@ -1,14 +1,25 @@
servicedir:=$(shell mktemp -d)
default: fs.lua init.lua nl.lua svc.lua net/constants.lua
default: fs.lua init.lua nl.lua svc.lua process.lua net/constants.lua
test:
CHECK=fs.fnl init.fnl svc.fnl process.fnl
check:
ln -s . anoia
fennel ./run-tests.fnl $(CHECK)
fennel test.fnl
fennel test-svc.fnl
fennel test-svc.fnl $(servicedir)
test -f $(servicedir)/fish
test "`cat $(servicedir)/fish`" = "food"
test -d $(servicedir)/nested/path
test "`cat $(servicedir)/nested/path/name`" = "value"
test "`cat $(servicedir)/nested/path/complex/attribute`" = "val"
test "`cat $(servicedir)/nested/path/complex/other`" = "42"
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 $< > $@
fennel --add-macro-path './assert.fnl' --compile $< > $@
+5 -1
View File
@@ -18,4 +18,8 @@
(.. "\nexpected " ve# "\ngot " va#)
))))
{ : expect : expect= }
(fn define-tests [& body]
(when _G.RUNNING_TESTS
`(do ,(unpack body))))
{ : define-tests : expect : expect= }
+2 -1
View File
@@ -1,4 +1,5 @@
{
bc, # for tests
fennel,
stdenv,
linotify,
@@ -11,7 +12,7 @@ in stdenv.mkDerivation {
inherit pname;
version = "0.1";
src = ./.;
nativeBuildInputs = [ fennel cpio ];
nativeBuildInputs = [ fennel cpio bc ];
buildInputs = with lua.pkgs; [ linotify lualinux ];
outputs = [ "out" "dev" ];
+30 -5
View File
@@ -1,4 +1,5 @@
(local ll (require :lualinux))
(import-macros { : define-tests : expect : expect= } :anoia.assert)
(local S_IFMT 0xf000)
(local S_IFSOCK 0xc000)
@@ -9,6 +10,12 @@
(local S_IFCHR 0x2000)
(local S_IFIFO 0x1000)
(macro errno-check [x]
`(match ,x
val# val#
(nil errno#) (assert nil (.. "system call failed, errno=" errno#))
))
(fn ifmt-bits [mode] (and mode (band mode 0xf000)))
(fn file-type [pathname]
@@ -33,14 +40,15 @@
(or (directory? pathname)
(let [parent (string.gsub pathname "/[^/]+/?$" "")]
(or (directory? parent) (mktree parent))
(assert (ll.mkdir pathname)))))
(errno-check (ll.mkdir pathname)))))
(fn dir [name]
(let [dp (assert (ll.opendir name) name)]
(let [dp (errno-check (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)))))
(case (ll.readdir dp)
(name filetype) (values name filetype)
(nil err) (do (if (> err 0) (print "ERR" err)) (ll.closedir dp) nil)
))))
(fn rmtree [pathname]
(case (file-type pathname)
@@ -58,6 +66,22 @@
unknown
(error (.. "can't remove " pathname " of mode \"" unknown "\""))))
;; lualinux doesn't publish access(2), this is not exactly
;; the same but will suffice until we can add it
(fn executable? [f]
(let [statbuf {}
stat (ll.lstat f statbuf 1)]
(and stat (> (band (. stat 3) 73) 0)))) ; \0111
(fn find-executable [exe search-path]
(accumulate [full-path nil
p (string.gmatch search-path "(.-):")]
(or full-path (let [f (.. p "/" exe)] (and (executable? f) f)))))
(define-tests
(let [p (find-executable "yes" (os.getenv "PATH"))]
(expect (string.match p "coreutils.+bin/yes$"))))
{
: mktree
@@ -65,5 +89,6 @@
: directory?
: dir
: file-type
: find-executable
:symlink (fn [from to] (ll.symlink from to))
}
+165 -22
View File
@@ -1,6 +1,14 @@
(fn assoc [tbl k v]
;; importing assert.fnl macros here would be circular, so we can't use
;; the full test functionality
(macro define-tests [& body]
(when _G.RUNNING_TESTS
`(do ,(unpack body))))
(fn assoc [tbl k v & more]
(tset tbl k v)
tbl)
(case more
[k v] (assoc tbl k v)
_ tbl))
(fn merge [table1 table2]
(collect [k v (pairs table2) &into table1]
@@ -34,31 +42,67 @@
c (str:gmatch ".")]
(+ (* h 33) (string.byte c))))
(fn table= [a b]
(if (= a b)
true
(and (= (type a) :table) (= (type b) :table)
(accumulate [equal true
k v1 (pairs a)
&until (not equal)]
;; all keys in a have the same value in a and b
(and equal
(let [v2 (. b k)] (and v2 (table= v1 v2)))))
(accumulate [present true
k _ (pairs b)
&until (not present)]
;; there are no keys in b which are not also in a
(and present (. a k))))))
(define-tests
(assert (table= {:a 1 :b 2} {:b 2 :a 1}))
(assert (not (table= {:a 1 :b 2 :k :l} {:b 2 :a 1})))
(assert (not (table= {:a 1 :b 2} {:b 2 :a 1 :k :l})))
(assert (table= {:a 1 :b {:l 17}} {:b {:l 17} :a 1}))
(assert (table= {:a [4 5 6 7] } {:a [4 5 6 7]}))
(assert (not (table= {:a [4 5 6 7] } {:a [4 5 6 7 8]})))
(assert (not (table= {:a [4 5 7 6] } {:a [4 5 6 7 ]})))
(assert (table= {} {}))
)
(fn dig [tree path]
(match path
[el & more] (dig (. tree el) more)
[el] (. tree el)
[] tree))
(fn %% [fmt ...] (string.format fmt ...))
(local
base64-indices
(doto [
"B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P"
"Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z" "a" "b" "c" "d" "e" "f"
"g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s" "t" "u" "v"
"w" "x" "y" "z" "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "-" "_"
]
(tset 0 "A")))
(let [base [
"B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P"
"Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z" "a" "b" "c" "d" "e" "f"
"g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s" "t" "u" "v"
"w" "x" "y" "z" "0" "1" "2" "3" "4" "5" "6" "7" "8" "9"
]]
{
:url
(merge (dup base)
{ 0 "A"
62 "-"
63 "_" })
:standard
(merge (dup base)
{ 0 "A"
62 "+"
63 "/" })
}))
;; local function base64(s)
;; local byte, rep = string.byte, string.rep
;; local pad = 2 - ((#s-1) % 3)
;; s = (s..rep('\0', pad)):gsub("...", function(cs)
;; local a, b, c = byte(cs, 1, 3)
;; return bs[a>>2] .. bs[(a&3)<<4|b>>4] .. bs[(b&15)<<2|c>>6] .. bs[c&63]
;; end)
;; return s:sub(1, #s-pad) .. rep('=', pad)
;; end
(fn base64url [s]
"URL-safe Base64-encoded form of s (no trailing padding)"
(fn base64-encode [s bs]
(let [pad (- 2 (% (- (# s) 1) 3))
bs base64-indices
blank (string.rep "\0" pad)
s (-> (.. s blank)
(: :gsub
@@ -69,13 +113,110 @@
(. bs (bor (lshift (band a 3) 4) (rshift b 4)))
(. bs (bor (lshift (band b 15) 2) (rshift c 6)))
(. bs (band c 63)))))))]
(s:sub 1 (- (# s) pad))))
(.. (s:sub 1 (- (# s) pad))
(string.rep "=" pad))))
(fn base64-decode [input rindices]
;; take groups of 4 characters, reverse-look them up in base64-indices,
;; convert to 24 bit number,
;; convert to three characters
;; things that might happen at the end of the string:
;; 1) if the input string length was n * 3, it will have been
;; encoded as n * 4 base64 digits, no special handling required
;; 2) if the input string length was (n * 3) + 2, the last 16 bits
;; are encoded into 3 base64 digits which are followed by an '='
;; 3) if the input string length was (n * 3) + 1, the last 8 bits
;; are encoded into 2 base64 digits which are followed by '=='
;; 0) there is never an '===' padding, because an 8 bit byte
;; can never be encoded into a single 6 bit base64 digit
(let [input (.. input (string.rep "=" (% (- 4 (# input)) 4)))
pad-chars (# (or (string.match input "(=+)$") ""))
trim-index (case pad-chars
0 -1
1 -2
2 -3
3 (assert nil "3 pad bytes??"))]
(->
(icollect [s (string.gmatch input "(....)")]
(let [(a b c d) (string.byte s 1 4)
ri (fn [x] (assert (. rindices x) (.. "invalid " x)))
n (bor (ri d)
(lshift (ri c) 6)
(lshift (ri b) 12)
(lshift (ri a) 18))]
(string.pack "bbb"
(rshift (band 0xff0000 n) 16)
(rshift (band 0x00ff00 n) 8)
(band 0x0000ff n))))
(table.concat "")
(string.sub 1 trim-index)
)))
(fn base64 [alphabet-des]
(let [alphabet (or (. base64-indices alphabet-des) alphabet-des (. base64-indices :standard) )
ralphabet (doto
(collect [k v (pairs alphabet)]
(values (string.byte v) k))
(tset (string.byte "=") 0))]
{
:encode (fn [_ str] (base64-encode str alphabet))
:decode (fn [_ str] (base64-decode str ralphabet))
}))
(fn base64url [str]
;; for backward-compatibility, this does not pad the encoded string
(let [encoded (: (base64 :url) :encode str)]
(pick-values 1 (string.gsub encoded "=+$" ""))))
(define-tests
(let [{: view} (require :fennel)
b64 (base64 :url)]
(let [a (b64:decode "YWxsIHlvdXIgYmFzZQ==")]
(assert (= a "all your base") (view a)))
(let [a (b64:decode "ZmVubmVsIHRoaW5n")]
(assert (= a "fennel thing") a))
(let [a (b64:decode "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms=")]
(assert (= a "Many hands make light work") (view a)))
(let [a (b64:encode "hello world")]
(assert (= a "aGVsbG8gd29ybGQ=") a))
(fn check [plain enc]
(let [a (b64:encode plain)] (assert (= a enc) (.. "encode " a)))
(let [a (b64:decode enc)] (assert (= a plain) (.. "decode " a))))
(check "" "")
(check "f" "Zg==")
(check "fo" "Zm8=")
(check "foo" "Zm9v")
(check "foob" "Zm9vYg==")
(check "fooba" "Zm9vYmE=")
(check "foobar" "Zm9vYmFy")
(let [x (b64:decode "REtOdUtNS05BNEJWLXdfcUhtNU9YV2liOUxkX3RTdVJTQWVUR0dkWldBdVEyaURObDZ2b3pSbEJwMzlzOEltdkhWdmpzZmMiLCJ5IjoiQVlDY1QwOGZrNFZWZ2lZSVIxbkU4UlJGaGZOSGdBUEFzckRITmJtRGNfUGtWZmdDR0xTMTIweU5SNncwdjd5RUY4WDN1OGpvazhkU0pqN0hnWjZCZHAzcSJ9LCJraWQiOiJlalVDaXBCUE9BeDRWQ1dQdUtkVGlYNDNadW5XTDNjSWN6V1h1RVZyTVNFIn0")]
(assert (string.match x "}$") x))
))
;; doesn't work if the padding is missing
;; (let [a (from-base64 "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcms")]
;; (assert (= a "Many hands make light work") (view a)))
; ))
{
: assoc
: base64
: base64url
: basename
: dig
: dirname
: dup
: file-exists?
@@ -83,4 +224,6 @@
: merge
: split
: system
: table=
: %%
}
+84
View File
@@ -0,0 +1,84 @@
(local ll (require :lualinux))
(local { : find-executable } (require :anoia.fs))
(import-macros { : define-tests : expect : expect= } :anoia.assert)
(macro errno-check [x]
`(match ,x
val# val#
(nil errno#) (assert nil (.. "system call failed, errno=" errno#))
))
(fn popen2 [pname argv envp]
(case (ll.pipe2)
(cin-s cin-d)
(match (ll.pipe2)
(cout-s cout-d)
(let [(pid err) (ll.fork)]
(if (not pid) (error (.. "error: " err))
(= pid 0)
(do
(ll.close cin-d)
(ll.close cout-s)
(ll.dup2 cin-s 0)
(ll.dup2 cout-d 1)
(ll.dup2 cout-d 2)
(ll.execve pname argv envp)
(error "execve failed"))
(> pid 0)
(do
(ll.close cin-s)
(ll.close cout-d)))
(values pid cin-d cout-s))
(nil err) (error (.. "popen pipe out: " err)))
(nil err) (error (.. "popen pipe in: " err))))
(fn spawn [pname argv envp callback]
(let [(pid in out) (popen2 pname argv envp)
pollfds [
(bor (lshift in 32) (lshift 4 16))
(bor (lshift out 32) (lshift 1 16))
]]
(while (or (> (. pollfds 1) 0) (> (. pollfds 2) 0))
(ll.poll pollfds)
(if
(> (band (. pollfds 2) 0x11) 0) ; POLLIN | POLLHUP
(if (not (callback :out out)) (tset pollfds 2 (lshift -1 32)))
(> (band (. pollfds 1) 4) 0) ; POLLOUT
(if (not (callback :in in)) (tset pollfds 1 (lshift -1 32)))
))
(match (ll.waitpid pid)
(0 status) false
(pid status) (rshift (band status 0xff00) 8)
(nil errno) (error (.. "waitpid: " errno)))))
(define-tests
(var buf "4 * 6\n") ;; spawn bc to multiply two numbers
(let [out []
p (spawn
(assert (find-executable "bc" (os.getenv "PATH")))
["bc"]
(ll.environ)
(fn [stream fd]
(match stream
:out (let [b (ll.read fd)]
(table.insert out b)
(> (# b) 0))
:in (if (> (# buf) 0)
(let [bytes (ll.write fd buf)]
(set buf (string.sub buf (+ bytes 1) -1))
true)
(do
(ll.close fd)
false))
:err (assert nil (ll.read fd)))))]
(expect= (table.concat out) "24\n"))
)
{
: popen2
: spawn
}
+10
View File
@@ -0,0 +1,10 @@
(local fennel (require :fennel))
(local specials (require :fennel.specials))
(local compiler-env
(doto (. (specials.make-compiler-env) :_G)
(tset "RUNNING_TESTS" true)))
(each [_ f (ipairs arg)]
(print :testing f)
(fennel.dofile f { :correlate true :compilerEnv compiler-env }))
+15 -4
View File
@@ -1,6 +1,6 @@
(local inotify (require :inotify))
(local { : file-exists? } (require :anoia))
(local { : file-type : dir &as fs } (require :anoia.fs))
(local { : file-exists? : dirname } (require :anoia))
(local { : file-type : dir : mktree &as fs } (require :anoia.fs))
(fn read-line [name]
(with-open [f (assert (io.open name :r) (.. "can't open file " name))]
@@ -18,6 +18,15 @@
inotify.IN_CLOSE_WRITE)
handle))
(fn write-value [pathname value]
(mktree (dirname pathname))
(match (type value)
"string"
(with-open [f (assert (io.open pathname :w) (.. "can't open " pathname))]
(f:write value))
"table" (each [k v (pairs value)]
(write-value (.. pathname "/" k) v))))
(fn read-value [pathname]
(case (file-type pathname)
nil nil
@@ -46,8 +55,10 @@
:wait #(watcher:read)
:ready? (fn [self]
(and (has-file? "state") (not (has-file? ".lock"))))
:output (fn [_ filename]
(read-value (.. directory "/" filename)))
:output (fn [_ filename new-value]
(if new-value
(write-value (.. directory "/" filename) new-value)
(read-value (.. directory "/" filename))))
:close #(watcher:close)
: events
}))
+9
View File
@@ -15,3 +15,12 @@
:3 {:attribute "a33"}
:5 {:attribute "a55"}
:6 {:attribute "a66"}})))
(let [dir (. arg 1)
ex2 (svc.open dir)]
(ex2:output "fish" "food")
(ex2:output "nested/path/name" "value")
(ex2:output "nested/path/complex" {
:attribute "val"
:other "42"
}))
+89
View File
@@ -0,0 +1,89 @@
(local { : view } (require :fennel))
(local { : assoc : split } (require :anoia))
(local ctx (require :openssl.ssl.context))
(local csr (require :openssl.x509.csr))
(local altname (require :openssl.x509.altname))
(local pkey (require :openssl.pkey))
(local xn (require :openssl.x509.name))
(local http (require :fetch))
(macro ncall [f]
`(case ,f
ok# ok#
(nil err#) (error err#)))
(fn x509-name [subj]
(let [n (xn.new)]
(each [_ c (ipairs (split "," subj))]
(let [(k v) (string.match c "(.-)=(.+)")]
(n:add k v)))
n))
(fn x509-altname [subj]
(let [an (altname.new)]
(each [_ c (ipairs (split "," subj))]
(let [(k v) (string.match c "(.-)=(.+)")]
(if (= k "CN") (an:add "DNS" v))))
an))
(fn parse-args [args]
(case args
["--secret" secret & rest]
(assoc (parse-args rest)
:secret (with-open [f (ncall (io.open secret :r))] (f:read "l")))
["--subject" subject & rest]
(assoc (parse-args rest) :subject subject)
["--key-out" pathname & rest]
(assoc (parse-args rest) :key-out pathname)
["--certificate-out" pathname & rest]
(assoc (parse-args rest) :certificate-out pathname)
[server] { : server }
_ {}))
(local options (parse-args arg))
(fn private-key []
(pkey.new { :type :rsa :bits 1024 }))
(fn signing-request [pk]
(doto (csr.new)
(: :setVersion 3)
(: :setSubject (x509-name options.subject))
(: :setSubjectAlt (x509-altname options.subject))
(: :setPublicKey pk)
(: :addAttribute :challengePassword [options.secret])
(: :sign pk)))
(fn http-post [url body]
(match
(http.request "POST" url
"" 0
"application/x-pem-file"
body)
s s
(nil code msg) (error (.. "Error " code " POST " url ": " msg))))
(fn run []
(let [pk (private-key)
csr (signing-request pk)
;; key-out (or options.key-out-handle io.stdout)
;; cert-out (or options.cert-out-handle io.stdout)
cert (http-post options.server (csr:toPEM))]
(with-open [f (ncall (io.open options.key-out :w))]
(f:write (pk:toPEM :private)))
(with-open [f (ncall (io.open options.certificate-out :w))]
(f:write cert))
(print "done")))
{ : run }
+36
View File
@@ -0,0 +1,36 @@
{
fetchurl,
writeFennel,
fennel,
fennelrepl,
runCommand,
lua,
anoia,
lualinux,
fetch-freebsd,
openssl,
luaossl',
stdenv
}:
let name = "certifix-client";
in stdenv.mkDerivation {
inherit name;
src = ./.;
buildInputs = [fetch-freebsd openssl lua];
buildPhase = "";
installPhase = ''
mkdir -p $out/bin
cp -p ${writeFennel name {
packages = [
fetch-freebsd
fennel
anoia
lualinux
luaossl'
] ;
mainFunction = "run";
} ./${name}.fnl } $out/bin/${name}
'';
}
+9
View File
@@ -0,0 +1,9 @@
(local { : view } (require :fennel))
(local ctx (require openssl.ssl.ctx))
(fn run []
(print "hey"))
{ : run }
+20 -2
View File
@@ -14,6 +14,9 @@ in {
uimage = callPackage ./kernel/uimage.nix { };
kernel = callPackage ./kernel { };
};
outputRef = service : path :
let h = { inherit service path; };
in x : h.${x};
callService = path : parameters :
let pkg = callPackage path {};
checkTypes = t : p : typeChecked (builtins.toString path) t p;
@@ -30,7 +33,7 @@ in {
};
lib = {
types =
let inherit (lib) types isDerivation;
let inherit (lib) mkOption types isDerivation;
in rec {
service = types.package // {
name = "service";
@@ -43,6 +46,10 @@ in {
description = "parametrisable s6-rc service definition";
check = x: lib.isAttrs x && x ? parameters && x ? build;
};
replacable = t : types.either
t
# function might return a service or a path
(types.functionTo types.anything);
};
inherit typeChecked;
};
@@ -53,7 +60,9 @@ in {
# please keep the rest of this list alphabetised :-)
anoia = callPackage ./anoia { };
certifix-client = callPackage ./certifix-client { };
devout = callPackage ./devout { };
fetch-freebsd = callPackage ./fetch-freebsd { };
fennel = callPackage ./fennel { };
fennelrepl = callPackage ./fennelrepl { };
firewallgen = callPackage ./firewallgen { };
@@ -63,11 +72,14 @@ in {
hi = callPackage ./hi { };
ifwait = callPackage ./ifwait { };
initramfs-peek = callPackage ./initramfs-peek { };
incz = callPackage ./incz { };
json-to-fstree = callPackage ./json-to-fstree { };
kernel-backport = callPackage ./kernel-backport { };
kmodloader = callPackage ./kmodloader { };
levitate = callPackage ./levitate { };
libubootenv = callPackage ./libubootenv { };
linotify = callPackage ./linotify { };
logshipper = callPackage ./logshipper { };
lualinux = callPackage ./lualinux { };
# we need to build real lzma instead of using xz, because the lzma
@@ -87,14 +99,17 @@ in {
odhcp-script = callPackage ./odhcp-script { };
odhcp6c = callPackage ./odhcp6c { };
openwrt = callPackage ./openwrt { };
output-template = callPackage ./output-template { };
ppp = callPackage ./ppp { };
pppoe = callPackage ./pppoe { };
preinit = callPackage ./preinit { };
pseudofile = callPackage ./pseudofile { };
routeros = callPackage ./routeros { };
rxi-json = callPackage ./rxi-json { };
run-liminix-vm = callPackage ./run-liminix-vm { };
s6-init-bin = callPackage ./s6-init-bin { };
s6-rc-database = callPackage ./s6-rc-database { };
s6-rc-round-robin = callPackage ./s6-rc-round-robin { };
s6-rc-up-tree = callPackage ./s6-rc-up-tree { };
# schnapps is written by Turris and provides a high-level interface
@@ -103,14 +118,17 @@ in {
# vendor OS, or even to derisk Liminix updates on that device
schnapps = callPackage ./schnapps { };
seedrng = callPackage ./seedrng {};
serviceFns = callPackage ./service-fns { };
swconfig = callPackage ./swconfig { };
systemconfig = callPackage ./systemconfig { };
tangc = callPackage ./tangc { };
tufted = callPackage ./tufted { };
uevent-watch = callPackage ./uevent-watch { };
usb-modeswitch = callPackage ./usb-modeswitch { };
watch-outputs = callPackage ./watch-outputs { };
watch-ssh-keys = callPackage ./watch-ssh-keys { };
writeAshScript = callPackage ./write-ash-script { };
writeAshScriptBin = callPackage ./write-ash-script/bin.nix { };
writeFennel = callPackage ./write-fennel { };
writeFennelScript = callPackage ./write-fennel-script { };
}
+10 -10
View File
@@ -129,12 +129,12 @@
(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))))
(let [addr (string.pack "=Hz" AF_LOCAL name)
fd (check-errno (ll.socket AF_LOCAL SOCK_STREAM 0))]
(os.remove name)
(check-errno (ll.bind fd addr))
(check-errno (ll.listen fd 32))
fd))
(fn pollfds-for [fds]
(icollect [_ v (ipairs fds)]
@@ -173,10 +173,10 @@
(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)))
(let [fd (check-errno (ll.socket AF_NETLINK SOCK_RAW NETLINK_KOBJECT_UEVENT))]
(check-errno (ll.bind fd (string.pack "I2I2I4I4" ; family pad pid groups
AF_NETLINK 0 0 groups)))
fd))
(fn event-loop []
(let [fds {}]
+386
View File
@@ -0,0 +1,386 @@
commit bd51aae2e40814ac2ae5801fd9f83f6a4a886fb1
Author: Daniel Barlow <dan@telent.net>
Date: Fri Aug 23 11:33:24 2024 +0100
add -U otion to set path to authorized_keys file
based on https://github.com/mkj/dropbear/pull/35
by Salvador Fandino sfandino@yahoo.com
- Allow authorized keys inside dirs with the sticky bit set
- Add option -U for customizing authorized_keys path
- Updated for dropbear 2024.85 (source files moved to src/)
- allow %u, %d, %n "format specifiers" in pathname so that the user's
username/homedir/uid can be embedded into the path
diff --git a/Makefile.in b/Makefile.in
index 5ebfca2..686fbfb 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -51,7 +51,7 @@ COMMONOBJS = $(patsubst %,$(OBJ_DIR)/%,$(_COMMONOBJS))
_SVROBJS=svr-kex.o svr-auth.o sshpty.o \
svr-authpasswd.o svr-authpubkey.o svr-authpubkeyoptions.o svr-session.o svr-service.o \
svr-chansession.o svr-runopts.o svr-agentfwd.o svr-main.o svr-x11fwd.o\
- svr-tcpfwd.o svr-authpam.o
+ svr-tcpfwd.o svr-authpam.o pathexpand.o
SVROBJS = $(patsubst %,$(OBJ_DIR)/%,$(_SVROBJS))
_CLIOBJS=cli-main.o cli-auth.o cli-authpasswd.o cli-kex.o \
diff --git a/manpages/dropbear.8 b/manpages/dropbear.8
index bdb2ea0..c8d450d 100644
--- a/manpages/dropbear.8
+++ b/manpages/dropbear.8
@@ -29,6 +29,9 @@ or automatically with the '-R' option. See "Host Key Files" below.
.B \-R
Generate hostkeys automatically. See "Host Key Files" below.
.TP
+.B \-U \fIauthorized_keys
+Absolute pathname to file containing authorized user keys. May contain the sequences %d, %n, %u which are expanded to the user's home directory, username and numeric uid respectively. Default '%d/.ssh/authorized_keys'.
+.TP
.B \-F
Don't fork into background.
.TP
diff --git a/src/pathexpand.c b/src/pathexpand.c
new file mode 100644
index 0000000..2028733
--- /dev/null
+++ b/src/pathexpand.c
@@ -0,0 +1,132 @@
+#include <limits.h>
+#include <string.h>
+#include <stdio.h>
+
+#ifdef TEST_PATHEXPAND
+
+/* to run tests:
+ gcc -Wall -o pathexpand -D TEST_PATHEXPAND=1 src/pathexpand.c && ./pathexpand
+*/
+
+char * pathexpand(char *relfilename);
+
+
+#define m_malloc(c) malloc(c)
+#define m_strdup(c) strdup(c)
+
+struct session {
+ struct AuthState {
+ char * pw_dir;
+ char * pw_name;
+ uid_t pw_uid;
+ } authstate;
+};
+
+struct session ses = {
+ .authstate = {
+ .pw_dir = "/home/dan",
+ .pw_name = "dan",
+ .pw_uid = 12345,
+ }
+};
+
+int exit_status = 0;
+
+int expect_expansion(char * input, char * expected) {
+ char *actual = pathexpand(input);
+ if(strcmp(actual, expected) != 0) {
+ printf("expected %s for %s, got %s\n", expected, input, actual);
+ exit_status++;
+ }
+ free(actual);
+ return exit_status;
+}
+
+int main(int argc, char *argv[]) {
+ for(int i = 1; i < argc; i++) {
+ char *actual = pathexpand(argv[i]);
+ printf("%s => %s\n", argv[i], pathexpand(argv[i]));
+ free(actual);
+ }
+
+ /* a string without % is unaltered */
+ expect_expansion("hello", "hello");
+
+ /* discards single trailing % */
+ expect_expansion("hello%", "hello");
+
+ /* %% is transformed to % */
+ expect_expansion("hello%%", "hello%");
+ expect_expansion("hello%%goodbye", "hello%goodbye");
+
+ /* %u is transformed to uid */
+ expect_expansion("/run/user/%u/authorized_keys", "/run/user/12345/authorized_keys");
+ /* % sequences work when at start of string */
+ expect_expansion("%u/authorized_keys", "12345/authorized_keys");
+
+ /* %d expands to home directory */
+ expect_expansion("%d/.ssh", "/home/dan/.ssh");
+
+ /* %n expands to username */
+ expect_expansion("/tmp/%n/.ssh", "/tmp/dan/.ssh");
+
+ /* unrecognised specifiers are discarded */
+ expect_expansion("/hi/%q/.ssh", "/hi//.ssh");
+
+ exit(exit_status);
+}
+
+#else
+#include "session.h"
+#include "debug.h"
+#endif
+#define NUMLEN(c) strlen(#c)
+
+char * pathexpand(char *relfilename)
+{
+ char * filename;
+ int len;
+
+ len = strlen(relfilename);
+ for(char *p = relfilename; p; p = strchr(p, '%')) {
+ switch(*(p+1)) {
+ case 'd': len += strlen(ses.authstate.pw_dir); break;
+ case 'n': len += strlen(ses.authstate.pw_name); break;
+ case 'u': len += NUMLEN(INT_MAX); break;
+ }
+ if(*(p+1) == '\0') break;
+ p=p+2;
+ }
+ filename = m_malloc(len+1);
+ filename[0] = '\0';
+
+ char *start = relfilename;
+ char *out = filename;
+ char *p = relfilename;
+ do {
+ p = strchrnul(start, '%');
+ strncat(out, start, p - start);
+
+ if(*p == '\0') break;
+
+ switch(*(p+1)) {
+ case '\0':
+ p++; break;
+ case 'd':
+ strcat(out, ses.authstate.pw_dir); break;
+ case 'n':
+ strcat(out, ses.authstate.pw_name); break;
+ case 'u':
+ snprintf(out + strlen(out),
+ NUMLEN(INT_MAX),
+ "%d",
+ ses.authstate.pw_uid);
+ break;
+ case '%':
+ strcat(out, "%"); break;
+ }
+ start = p+2;
+ }
+ while (*p);
+ return filename; /* caller must free */
+}
diff --git a/src/runopts.h b/src/runopts.h
index 1c88b5c..707008f 100644
--- a/src/runopts.h
+++ b/src/runopts.h
@@ -128,7 +128,8 @@ typedef struct svr_runopts {
char * pidfile;
char * forced_command;
- char* interface;
+ char * authkeysfile;
+ char * interface;
#if DROPBEAR_PLUGIN
/* malloced */
diff --git a/src/svr-authpubkey.c b/src/svr-authpubkey.c
index 5d298cb..54502f4 100644
--- a/src/svr-authpubkey.c
+++ b/src/svr-authpubkey.c
@@ -73,7 +73,7 @@
static int checkpubkey(const char* keyalgo, unsigned int keyalgolen,
const unsigned char* keyblob, unsigned int keybloblen);
-static int checkpubkeyperms(void);
+static int checkpubkeyperms(char *filename, char *base);
static void send_msg_userauth_pk_ok(const char* sigalgo, unsigned int sigalgolen,
const unsigned char* keyblob, unsigned int keybloblen);
static int checkfileperm(char * filename);
@@ -431,6 +431,7 @@ out:
return ret;
}
+extern char *pathexpand(char *input);
/* Checks whether a specified publickey (and associated algorithm) is an
* acceptable key for authentication */
@@ -458,19 +459,12 @@ static int checkpubkey(const char* keyalgo, unsigned int keyalgolen,
dropbear_exit("Failed to set euid");
}
#endif
+ filename = pathexpand(svr_opts.authkeysfile);
+
/* check file permissions, also whether file exists */
- if (checkpubkeyperms() == DROPBEAR_FAILURE) {
- TRACE(("bad authorized_keys permissions, or file doesn't exist"))
+ if (checkpubkeyperms(filename, ses.authstate.pw_dir) == DROPBEAR_FAILURE) {
+ TRACE(("bad authorized keys permissions on %s, or file doesn't exist", filename))
} else {
- /* we don't need to check pw and pw_dir for validity, since
- * its been done in checkpubkeyperms. */
- len = strlen(ses.authstate.pw_dir);
- /* allocate max required pathname storage,
- * = path + "/.ssh/authorized_keys" + '\0' = pathlen + 22 */
- filename = m_malloc(len + 22);
- snprintf(filename, len + 22, "%s/.ssh/authorized_keys",
- ses.authstate.pw_dir);
-
authfile = fopen(filename, "r");
if (!authfile) {
TRACE(("checkpubkey: failed opening %s: %s", filename, strerror(errno)))
@@ -486,7 +480,7 @@ static int checkpubkey(const char* keyalgo, unsigned int keyalgolen,
if (authfile == NULL) {
goto out;
}
- TRACE(("checkpubkey: opened authorized_keys OK"))
+ TRACE(("checkpubkey: opened %s OK", filename))
line = buf_new(MAX_AUTHKEYS_LINE);
line_num = 0;
@@ -524,53 +518,47 @@ out:
/* Returns DROPBEAR_SUCCESS if file permissions for pubkeys are ok,
* DROPBEAR_FAILURE otherwise.
- * Checks that the user's homedir, ~/.ssh, and
- * ~/.ssh/authorized_keys are all owned by either root or the user, and are
+ * Checks filename and its parent directories recursively until the
+ * base directory (usually ~/) or one of its ancestors (up to /) is
+ * reached.
+ * The files and directories must be all owned by root or the user, and be
* g-w, o-w */
-static int checkpubkeyperms() {
-
- char* filename = NULL;
+static int checkpubkeyperms(char *filename, char *base) {
+ char* path = NULL;
int ret = DROPBEAR_FAILURE;
unsigned int len;
- TRACE(("enter checkpubkeyperms"))
-
- if (ses.authstate.pw_dir == NULL) {
- goto out;
- }
+ TRACE(("enter checkpubkeyperms(%s, %s)", filename, base))
- if ((len = strlen(ses.authstate.pw_dir)) == 0) {
+ if ((base == NULL) || (base[0] != '/') ||
+ (filename == NULL) || (filename[0] != '/')) {
+ /* both filename and base must be absolute paths */
goto out;
}
- /* allocate max required pathname storage,
- * = path + "/.ssh/authorized_keys" + '\0' = pathlen + 22 */
- len += 22;
- filename = m_malloc(len);
- strlcpy(filename, ses.authstate.pw_dir, len);
+ len = strlen(filename);
+ path = m_strdup(filename);
- /* check ~ */
- if (checkfileperm(filename) != DROPBEAR_SUCCESS) {
- goto out;
- }
-
- /* check ~/.ssh */
- strlcat(filename, "/.ssh", len);
- if (checkfileperm(filename) != DROPBEAR_SUCCESS) {
- goto out;
- }
+ while (checkfileperm(len ? path : "/") == DROPBEAR_SUCCESS) {
+ /* check if we are on base trail and if this is the
+ * case, return success */
+ if ((strncmp(base, path, len) == 0) &&
+ (!len || (base[len] == '\0') || (base[len] == '/'))) {
+ ret = DROPBEAR_SUCCESS;
+ break;
+ }
- /* now check ~/.ssh/authorized_keys */
- strlcat(filename, "/authorized_keys", len);
- if (checkfileperm(filename) != DROPBEAR_SUCCESS) {
- goto out;
+ /* look for parent directory */
+ while (--len) {
+ if (path[len] == '/') {
+ path[len] = '\0';
+ break;
+ }
+ }
}
- /* file looks ok, return success */
- ret = DROPBEAR_SUCCESS;
-
out:
- m_free(filename);
+ m_free(path);
TRACE(("leave checkpubkeyperms"))
return ret;
@@ -596,7 +584,9 @@ static int checkfileperm(char * filename) {
TRACE(("wrong ownership"))
}
/* check permissions - don't want group or others +w */
- if (filestat.st_mode & (S_IWGRP | S_IWOTH)) {
+ /* (unless sticky dir, which is allowed) */
+ if ((filestat.st_mode & (S_IWGRP | S_IWOTH)) &&
+ !(S_ISDIR(filestat.st_mode) && (filestat.st_mode & S_ISVTX))) {
badperm = 1;
TRACE(("wrong perms"))
}
diff --git a/src/svr-runopts.c b/src/svr-runopts.c
index c4f83c1..faddfa2 100644
--- a/src/svr-runopts.c
+++ b/src/svr-runopts.c
@@ -147,6 +147,7 @@ void svr_getopts(int argc, char ** argv) {
char* maxauthtries_arg = NULL;
char* reexec_fd_arg = NULL;
char* keyfile = NULL;
+ char* authkeysfile = NULL;
char c;
#if DROPBEAR_PLUGIN
char* pubkey_plugin = NULL;
@@ -173,6 +174,8 @@ void svr_getopts(int argc, char ** argv) {
svr_opts.hostkey = NULL;
svr_opts.delay_hostkey = 0;
svr_opts.pidfile = expand_homedir_path(DROPBEAR_PIDFILE);
+ svr_opts.authkeysfile = "%d/.ssh/authorized_keys";
+
#if DROPBEAR_SVR_LOCALANYFWD
svr_opts.nolocaltcp = 0;
#endif
@@ -322,6 +325,9 @@ void svr_getopts(int argc, char ** argv) {
case 'u':
/* backwards compatibility with old urandom option */
break;
+ case 'U':
+ next = &authkeysfile;
+ break;
#if DROPBEAR_PLUGIN
case 'A':
next = &pubkey_plugin;
@@ -372,6 +378,10 @@ void svr_getopts(int argc, char ** argv) {
addhostkey(keyfile);
keyfile = NULL;
}
+ if (authkeysfile) {
+ svr_opts.authkeysfile = m_strdup(authkeysfile);
+ authkeysfile = NULL;
+ }
}
}
+15 -4
View File
@@ -27,17 +27,28 @@ in writeScriptBin "fennelrepl" ''
package.path = ${lib.strings.escapeShellArg luapath} .. ";" .. package.path
package.cpath = ${lib.strings.escapeShellArg luacpath} .. ";" .. (package.cpath or "")
local fennel = require "fennel"
local specials = require("fennel.specials")
table.insert(package.loaders or package.searchers,1, fennel.searcher)
fennel['macro-path'] = "${anoia.dev}/share/lua/${lua.luaversion}/?.fnl;" .. fennel['macro-path']
local function eval_as_test(f)
local g = (specials["make-compiler-env"]())._G
g["RUNNING_TESTS"] = true
return fennel.dofile(f, {correlate = true, compilerEnv = g})
end
local more_fennel = os.getenv("FENNEL_PATH")
if more_fennel then
fennel.path = more_fennel .. ";" .. fennel.path
fennel.path = more_fennel .. ";" .. fennel.path
end
if #arg > 0 then
script = table.remove(arg, 1)
fennel.dofile(script, {correlate = true}, arg)
if arg[1] == '--test' then
eval_as_test(arg[2])
else
script = table.remove(arg, 1)
fennel.dofile(script, {correlate = true}, arg)
end
else
fennel.repl()
fennel.repl()
end
''
+31
View File
@@ -0,0 +1,31 @@
{
cmake,
stdenv,
openssl,
lua,
fetchFromGitHub
}: stdenv.mkDerivation {
pname = "fetch-freebsd";
version = "v12.0.11";
src = fetchFromGitHub {
owner= "jrmarino";
repo = "fetch-freebsd";
rev = "v12.0.11";
hash = "sha256-nLNqjQFV9x2NntBdUlabxjS9q+er28zi8uXjWvCK2Ps=";
};
cmakeFlags = [
"-DFETCH_PROGRAM=OFF"
"-DFETCH_LIBRARY=ON"
"-DUSE_SYSTEM_SSL=ON"
];
postBuild = ''
$CC -shared -o fetch-lua.so ${./lua-glue.c} -I$src -Llibrary -lssl -lfetch -llua
'';
nativeBuildInputs = [ cmake ];
buildInputs = [ lua openssl ];
postInstall = ''
rm -r $out/lib/lib*.a
mkdir -p $out/lib/lua/${lua.luaversion}
install fetch-lua.so $out/lib/lua/${lua.luaversion}/fetch.so
'';
}
+114
View File
@@ -0,0 +1,114 @@
#include <stdlib.h>
#include <string.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include "fetch.h"
/*
FIXME: "By default libfetch allows TLSv1 and newer when negotiating
the connecting with the remote peer. You can change this behavior
by setting the SSL_NO_TLS1, SSL_NO_TLS1_1 and SSL_NO_TLS1_2
environment variables to disable TLS 1.0, 1.1 and 1.2 respectively."
*/
int lp_error(lua_State *L, int code, char *message) {
lua_pushnil(L);
lua_pushinteger(L, code);
lua_pushlstring(L, message, strlen(message));
return 3;
}
int lp_fetched(lua_State *L, FXRETTYPE fp, struct url_stat *stat) {
size_t bytes_read = 0;
char * content = 0;
if(stat->size > 0) {
content = (char *) malloc(stat->size);
es_read(fp, content, stat->size, &bytes_read);
if(bytes_read != stat->size) {
lp_error(L, -1, "bytes read does not match content-length");
}
} else {
/* no content-length header */
int block_size = 128;
int buf_size = block_size;
size_t bytes_read_block;
do {
content = (char *) realloc(content, buf_size);
es_read(fp, content + bytes_read, block_size, &bytes_read_block);
bytes_read += bytes_read_block;
buf_size += block_size;
} while(bytes_read_block > 0);
}
lua_pushlstring(L, (const char *) content, bytes_read);
lua_newtable(L);
lua_pushliteral(L, "last-modified");
lua_pushinteger(L, stat->mtime);
lua_settable(L, -3);
/* free(content) ? */
return 2;
}
int lp_fetch(lua_State *L) {
const char * url_string = luaL_checkstring(L, 1);
const char * flags = luaL_optstring(L, 2, "");
time_t if_modified_since = luaL_optinteger(L, 3, 0);
struct url *url = fetchParseURL(url_string);
if(url == NULL) {
return lp_error(L, -1, "url not parseable");
}
url->ims_time = if_modified_since;
struct url_stat stat;
FXRETTYPE fp = fetchXGet(url, &stat, flags);
fetchFreeURL(url);
if(fp == NULL) {
return lp_error(L, fetchLastErrCode, fetchLastErrString);
}
return lp_fetched(L, fp, &stat);
}
int lp_request(lua_State *L) {
const char * request_method = luaL_checkstring(L, 1);
const char * url_string = luaL_checkstring(L, 2);
const char * flags = luaL_optstring(L, 3, "");
time_t if_modified_since = luaL_optinteger(L, 4, 0);
const char * content_type = lua_tostring(L, 5);
const char * body = lua_tostring(L, 6);
struct url *url = fetchParseURL(url_string);
if(url == NULL) {
return lp_error(L, -1, "url not parseable");
}
url->ims_time = if_modified_since;
struct url_stat stat;
FXRETTYPE fp = http_request_body(url,
request_method,
&stat,
NULL, /* no proxy */
flags,
content_type,
body);
fetchFreeURL(url);
if(fp == NULL) {
return lp_error(L, fetchLastErrCode, fetchLastErrString);
}
return lp_fetched(L, fp, &stat);
}
static const struct luaL_Reg funcs [] = {
{"fetch", lp_fetch},
{"request", lp_request},
{NULL, NULL} /* sentinel */
};
int luaopen_fetch (lua_State *L) {
luaL_newlib(L, funcs);
return 1;
}
+1 -1
View File
@@ -31,7 +31,7 @@ let
indent = text : indentLines 0 (splitString "\n" text);
dochain = { name, type, rules,
dochain = { name, type, family, rules,
policy ? null,
priority ? "filter",
hook ? null } : ''
+4 -5
View File
@@ -1,15 +1,14 @@
{
netlink-lua,
writeFennelScript,
writeFennel,
runCommand,
anoia,
}:
runCommand "ifwait" { } ''
mkdir -p $out/bin
cp -p ${
writeFennelScript "ifwait" [
anoia
netlink-lua
] ./ifwait.fnl
writeFennel "ifwait" {
packages = [ anoia netlink-lua ];
} ./ifwait.fnl
} $out/bin/ifwait
''
+38
View File
@@ -0,0 +1,38 @@
{
fetchurl,
writeFennel,
fennel,
fennelrepl,
runCommand,
lua,
anoia,
lualinux,
stdenv
}:
let name = "incz";
in stdenv.mkDerivation {
inherit name;
src = ./.;
buildInputs = [lua];
nativeBuildInputs = [fennelrepl];
buildPhase = ''
fennelrepl --test ./incz.fnl
cp -p ${writeFennel name {
packages = [
anoia
lualinux
fennel
];
macros = [
anoia.dev
];
mainFunction = "run";
} ./incz.fnl } ${name}
'';
installPhase = ''
install -D ${name} $out/bin/${name}
'';
}
+58
View File
@@ -0,0 +1,58 @@
(local { : base64 } (require :anoia))
(local ll (require :lualinux))
(fn index [str indexname]
(string.format "{\"index\":{\"_index\":%q}}\n%s" indexname str))
(local crlf "\r\n")
(fn chunk [str]
(let [len (# str)]
(string.format "%x%s%s%s" len crlf str crlf)))
(fn http-header [host path auth]
(string.format
"POST %s HTTP/1.1\r
Host: %s\
Authorization: basic %s
Transfer-Encoding: chunked\r
\r
"
path host
(let [b64 (base64 :url)] (b64:encode auth))))
(fn format-timestamp [timestamp]
;; I can't make zincsearch understand any epoch-based timestamp
;; formats, so we are formatting dates as iso-8601 and damn the leap
;; seconds :-(
(let [secs (- (tonumber (string.sub timestamp 1 16) 16)
(lshift 1 62))
nano (tonumber (string.sub timestamp 16 24) 16)
ts (+ (* secs 1000) (math.floor (/ nano 1000000)))]
(.. (os.date "!%FT%T" secs) "." nano "Z")))
(fn process-line [indexname hostname line]
(let [(timestamp service msg) (string.match line "@(%x+) (%g+) (.+)$")]
(->
(string.format
"{%q:%q,%q:%q,%q:%q,%q:%q}\n"
"@timestamp" (format-timestamp timestamp)
:service service
:message msg
:host hostname)
(index indexname)
chunk)))
(fn run []
(let [myhostname (with-open [h (io.popen "hostname" :r)] (h:read "l"))
(filename loghost credentials indexname) (table.unpack arg)]
(with-open [infile (assert (io.open filename :r))]
(ll.write 1 (http-header loghost "/api/_bulk" credentials))
(while (case (infile:read "l")
line (ll.write 1 (process-line indexname myhostname line))))
(ll.write 1 (chunk "")))
(io.stderr:write (ll.read 0))))
{ : run }
+37
View File
@@ -0,0 +1,37 @@
{
fetchurl,
writeFennel,
fennel,
fennelrepl,
runCommand,
lua,
anoia,
linotify,
lualinux,
fetch-freebsd,
openssl,
rxi-json,
stdenv
}:
let name = "json-to-fstree";
in stdenv.mkDerivation {
inherit name;
src = ./.;
buildInputs = [fetch-freebsd rxi-json openssl lua];
buildPhase = "";
installPhase = ''
mkdir -p $out/bin
cp -p ${writeFennel name {
packages = [
fetch-freebsd
rxi-json
anoia
lualinux
linotify
] ;
mainFunction = "run";
} ./${name}.fnl } $out/bin/${name}
'';
}
+30
View File
@@ -0,0 +1,30 @@
(local json (require :json))
(local http (require :fetch))
(local svc (require :anoia.svc))
(local { : utime } (require :lualinux))
(fn download [url dest]
(let [state (.. dest "/state")
previously (ll.lstat state 12)]
(match (http.fetch url "i" previously)
(nil 10 _) ; not modified
(print (.. url " not modified, already up to date"))
(nil code str)
(assert nil (.. "error " code ": " str))
(body { : last-modified })
(let [service (svc.open dest)
lock (.. dest "/.lock")]
(with-open [fout (io.open lock :w)] (fout:write ""))
(service:output "." (json.decode body))
(with-open [fout (io.open state :w)] (fout:write "ok"))
(utime state last-modified)
(os.remove lock))
)))
(fn run [] (download (. arg 1) (. arg 2)))
{ : run }
+1
View File
@@ -62,6 +62,7 @@ let
../../modules/busybox.nix
base
({ ... } : paramConfig)
({ ... } : { config = { logging.script = "t"; }; })
../../modules/s6
];
};

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