diff --git a/examples/l2tp.nix b/examples/l2tp.nix index 169e62f..7d41e92 100644 --- a/examples/l2tp.nix +++ b/examples/l2tp.nix @@ -28,6 +28,9 @@ in rec { ../modules/watchdog ../modules/mount ../modules/ppp + ../modules/round-robin + ../modules/health-check + ../modules/profiles/gateway.nix ]; hostname = "thing"; @@ -38,7 +41,95 @@ in rec { authType = "chap"; }; - services.dhcpc = svc.network.dhcp.client.build { + 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 = { + interface = let + pppoe = svc.pppoe.build { + interface = config.hardware.networkInterfaces.wan; + debug = true; + username = rsecrets.l2tp.name; + password = rsecrets.l2tp.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; + ppp-options = [ + "debug" "+ipv6" "noauth" + "name" rsecrets.l2tp.name + "password" rsecrets.l2tp.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; + "${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; + }; + }; + + services.bootstrap-dhcpc = svc.network.dhcp.client.build { interface = config.services.wwan; dependencies = [ config.services.hostname ]; }; diff --git a/modules/health-check/default.nix b/modules/health-check/default.nix new file mode 100644 index 0000000..9ac8858 --- /dev/null +++ b/modules/health-check/default.nix @@ -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"]; +} diff --git a/modules/health-check/service.nix b/modules/health-check/service.nix new file mode 100644 index 0000000..80b3e34 --- /dev/null +++ b/modules/health-check/service.nix @@ -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 ]; +}) diff --git a/overlay.nix b/overlay.nix index dda4ddc..52b5872 100644 --- a/overlay.nix +++ b/overlay.nix @@ -47,7 +47,8 @@ in extraPkgs // { # liminix library functions lim = { - parseInt = s : (builtins.fromTOML "r=${s}").r; + parseInt = s: (builtins.fromTOML "r=${s}").r; + orEmpty = x: if x != null then x else []; }; # keep these alphabetical diff --git a/pkgs/default.nix b/pkgs/default.nix index 75ecb28..1089295 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -91,6 +91,7 @@ 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 {}; diff --git a/pkgs/output-template/Makefile b/pkgs/output-template/Makefile new file mode 100644 index 0000000..b0d6b6c --- /dev/null +++ b/pkgs/output-template/Makefile @@ -0,0 +1,3 @@ +check: + ./output-template '{{' '}}' < example.ini > output + diff -u output example.ini.expected diff --git a/pkgs/output-template/default.nix b/pkgs/output-template/default.nix new file mode 100644 index 0000000..19566e4 --- /dev/null +++ b/pkgs/output-template/default.nix @@ -0,0 +1,34 @@ +{ + fetchurl, + writeFennel, + fennel, + runCommand, + lua, + anoia, + linotify, + lualinux, + stdenv +}: +let name = "output-template"; +in stdenv.mkDerivation { + inherit name; + src = ./.; + + buildInputs = [lua]; + doCheck = true; + + buildPhase = '' + cp -p ${writeFennel name { + packages = [ + anoia + lualinux + linotify + ] ; + mainFunction = "run"; + } ./output-template.fnl } ${name} + ''; + checkPhase = "make check"; + installPhase = '' + install -D ${name} $out/bin/${name} + ''; +} diff --git a/pkgs/output-template/example-service/.outputs/addresses/1/attribute b/pkgs/output-template/example-service/.outputs/addresses/1/attribute new file mode 100644 index 0000000..b54f8c3 --- /dev/null +++ b/pkgs/output-template/example-service/.outputs/addresses/1/attribute @@ -0,0 +1 @@ +a11 diff --git a/pkgs/output-template/example-service/.outputs/addresses/3/attribute b/pkgs/output-template/example-service/.outputs/addresses/3/attribute new file mode 100644 index 0000000..711c1a1 --- /dev/null +++ b/pkgs/output-template/example-service/.outputs/addresses/3/attribute @@ -0,0 +1 @@ +a33 diff --git a/pkgs/output-template/example-service/.outputs/addresses/5/attribute b/pkgs/output-template/example-service/.outputs/addresses/5/attribute new file mode 100644 index 0000000..8092345 --- /dev/null +++ b/pkgs/output-template/example-service/.outputs/addresses/5/attribute @@ -0,0 +1 @@ +a55 diff --git a/pkgs/output-template/example-service/.outputs/addresses/6/attribute b/pkgs/output-template/example-service/.outputs/addresses/6/attribute new file mode 100644 index 0000000..9139598 --- /dev/null +++ b/pkgs/output-template/example-service/.outputs/addresses/6/attribute @@ -0,0 +1 @@ +a66 diff --git a/pkgs/output-template/example-service/.outputs/colours/black b/pkgs/output-template/example-service/.outputs/colours/black new file mode 100644 index 0000000..c4949eb --- /dev/null +++ b/pkgs/output-template/example-service/.outputs/colours/black @@ -0,0 +1 @@ +000000 diff --git a/pkgs/output-template/example-service/.outputs/colours/blue b/pkgs/output-template/example-service/.outputs/colours/blue new file mode 100644 index 0000000..df785ee --- /dev/null +++ b/pkgs/output-template/example-service/.outputs/colours/blue @@ -0,0 +1 @@ +0000ff diff --git a/pkgs/output-template/example-service/.outputs/colours/green b/pkgs/output-template/example-service/.outputs/colours/green new file mode 100644 index 0000000..9f07fb6 --- /dev/null +++ b/pkgs/output-template/example-service/.outputs/colours/green @@ -0,0 +1 @@ +00ff00 diff --git a/pkgs/output-template/example-service/.outputs/colours/red b/pkgs/output-template/example-service/.outputs/colours/red new file mode 100644 index 0000000..db6f1d8 --- /dev/null +++ b/pkgs/output-template/example-service/.outputs/colours/red @@ -0,0 +1 @@ +ff0000 diff --git a/pkgs/output-template/example-service/.outputs/name b/pkgs/output-template/example-service/.outputs/name new file mode 100644 index 0000000..dbf1b39 --- /dev/null +++ b/pkgs/output-template/example-service/.outputs/name @@ -0,0 +1 @@ +eth1 diff --git a/pkgs/output-template/example.ini b/pkgs/output-template/example.ini new file mode 100644 index 0000000..c968fce --- /dev/null +++ b/pkgs/output-template/example.ini @@ -0,0 +1,3 @@ +wpa_passphrase={{ output("./example-service","colours/black") }} +think = {{ string.format("%q", output("./example-service","colours/blue")) }} +argonaut = {{ json_quote "hello\ngoodbye\tnext\027" }} diff --git a/pkgs/output-template/example.ini.expected b/pkgs/output-template/example.ini.expected new file mode 100644 index 0000000..ba1960c --- /dev/null +++ b/pkgs/output-template/example.ini.expected @@ -0,0 +1,3 @@ +wpa_passphrase=000000 +think = "0000ff" +argonaut = "hello\ngoodbye\tnext\u001B" diff --git a/pkgs/output-template/output-template.fnl b/pkgs/output-template/output-template.fnl new file mode 100644 index 0000000..95be3c6 --- /dev/null +++ b/pkgs/output-template/output-template.fnl @@ -0,0 +1,44 @@ +(local svc (require :anoia.svc)) + +(fn json-escape [s] + ;; All Unicode characters may be placed within the quotation marks, + ;; except for the characters that MUST be escaped: + ;; quotation mark, reverse solidus, and the control characters (U+0000 + ;; through U+001F). (RFC 8259) + (-> s + (string.gsub + "[\"\b\f\n\r\t]" { + "\b" "\\b" + "\"" "\\\"" + "\f" "\\f" + "\n" "\\n" + "\r" "\\r" + "\t" "\\t" + }) + (string.gsub + "([\x00-\x1b])" + (fn [x] (string.format "\\u%04X" (string.byte x)))))) + + +(fn substitute [text opening closing] + (let [delim (.. opening "(.-)" closing) + myenv { + : string + :output + (fn [service-path path] + (let [s (assert (svc.open (.. service-path "/.outputs")))] + (s:output path))) + :lua_quote #(string.format "%q" %1) + :json_quote (fn [x] (.. "\"" (json-escape x) "\"")) + }] + (string.gsub text delim + (fn [x] + (assert ((load (.. "return " x) x :t myenv)) + (string.format "missing value for %q" x)))))) + +(fn run [] + (let [[opening closing] arg + out (substitute (: (io.input) :read "*a") opening closing)] + (io.write out))) + +{ : run }