Compare commits
21 Commits
7e2b0068e6
...
9ab77a7d7e
Author | SHA1 | Date | |
---|---|---|---|
9ab77a7d7e | |||
c6918fec00 | |||
d4e46dbe28 | |||
d1f87a56e0 | |||
8c39b47cae | |||
2c7a16d792 | |||
d6b06abb63 | |||
6b32aa569e | |||
234d1bd87e | |||
c38f180fb7 | |||
9a8b22997c | |||
c32d09bd83 | |||
6649ebeccd | |||
929226ed9e | |||
a98f026210 | |||
f4dc001b71 | |||
024c018262 | |||
e1293e3778 | |||
0c406058e9 | |||
19d441333c | |||
a726c09ae4 |
176
THOUGHTS.txt
176
THOUGHTS.txt
@ -7020,3 +7020,179 @@ Sun Feb 9 21:33:57 GMT 2025
|
||||
nft update set @lan
|
||||
|
||||
echo 'flush set table-ip lan; add element table-ip lan { eth0,lo }' | nft -f -
|
||||
|
||||
Tue Feb 11 18:30:09 GMT 2025
|
||||
|
||||
outstanding for 1.0:
|
||||
|
||||
1) security audit fedback
|
||||
|
||||
a) ask ROS if I can ship their report, with a response doc
|
||||
showing the commits that address each finding/non-finding
|
||||
b) firewall rules: icmp rate limit, DNS, doc for icmpv6 packet dropping
|
||||
c) look over env var inputs and parse them properly instead of
|
||||
string glommeration
|
||||
|
||||
2) docs:
|
||||
- for each device, add "finishedness" status and link to build status
|
||||
- generally read them over and spruce up
|
||||
- porting guide
|
||||
|
||||
3) some kconfig magic to generate minimal kconfig files so that
|
||||
device modules don't end up as copy-pastes of the openwrt defconfig
|
||||
|
||||
|
||||
---
|
||||
|
||||
apparently 5% of available bandwidth is a reasonable rate limit for
|
||||
icmp
|
||||
|
||||
% nft add rule filter input limit rate over 10 mbytes/second drop
|
||||
|
||||
but nftables has no way to get interface bandwidth and indeed nor does
|
||||
the device generally: the 1000Mb/s ethernet interface might be
|
||||
connected to a 70Mb/s pppoe upstream and how would it know? So the
|
||||
site operator needs to say somewhere what the upstream bandwidth is.
|
||||
|
||||
Sun Feb 16 22:16:29 GMT 2025
|
||||
|
||||
we probably didn't need to write that service, we could have used the
|
||||
thing that makes templated config files _and_ if we somehow contrive
|
||||
to write the interface bandwidth as an interface output we could get
|
||||
that the same way
|
||||
|
||||
if only I could remember how it worked :-)
|
||||
|
||||
----
|
||||
|
||||
* watch-output watches only _one_ service and is called with a list of
|
||||
outputs inside that service, so not exactly what we need. we can
|
||||
extend it easily enough to watch multiple services using poll() if we
|
||||
can figure out the syntax we want. Luckily all the places that call it
|
||||
go through modules/secrets/subscriber.nix so it's easy enough to change
|
||||
existing uses
|
||||
|
||||
we could do
|
||||
watch-outputs -r foo /nix/store/blah/.outputs/ifname /nix/store/eee/.outputs/ifname ...
|
||||
|
||||
or
|
||||
watch-outputs -r foo /nix/store/blah:ifname /nix/store/eee:ifname /nix/store/eee:bandwidth
|
||||
|
||||
or
|
||||
|
||||
watch-outputs -r foo /nix/store/blah:ifname /nix/store/eee:ifname:bandwidth
|
||||
|
||||
which I quite like insofar as it's shorter but has no other real merit
|
||||
|
||||
then we need to decide how to represent an output reference in a firewall rule.
|
||||
Since each rule is basically text already, might just put the handlebars straight in
|
||||
|
||||
let qq = builtins.toJSON ;
|
||||
in "icmp6 limit rate over {{ tonumber(output(${qq (intf "service")}, ${qq (intf "bandwidth")})) / 20 }} bytes/second drop"
|
||||
|
||||
probably we should do a separate rule for each interface in the wan zone
|
||||
|
||||
Sun Feb 23 00:34:34 GMT 2025
|
||||
|
||||
looks like we have no tests for anything involving watched services or subscribers,
|
||||
or if we do I can't see what
|
||||
|
||||
Thu Feb 27 20:47:03 GMT 2025
|
||||
|
||||
- use output-template to write firewall rule file
|
||||
- wrap firewall in svc.secrets.subscriber.build (c.f. e745991) with zones as
|
||||
watched services
|
||||
- put the handlebars in the firewall config
|
||||
|
||||
we have uncommitted changes to watch-outputs that I'm relunctant to
|
||||
commit until I have some way to see if they're working. the pppoe test
|
||||
will check both firewall zones so _should_ start to fail with the
|
||||
current watch-outputs (because only one service) and then pass when we
|
||||
put the new one in
|
||||
|
||||
Fri Feb 28 01:00:03 GMT 2025
|
||||
|
||||
Well, it works at least well enough to pass the test. There is an awful hack
|
||||
though, because nftables doesn't accept "elements = { }" as valid syntax
|
||||
for a set with no elements, so we post-process the file to wipe those lines
|
||||
|
||||
I wonder if we could instead create the set empty and then use the "other"
|
||||
nftables format to generate commands that add the elements. If it's
|
||||
all in the same file (or included files) it will continue to be atomic
|
||||
|
||||
Other options
|
||||
|
||||
- is the nftables json format any better? we will have to rebuild it
|
||||
with json support, may be bugger
|
||||
- write lua bindings to libnftables
|
||||
|
||||
Fri Feb 28 23:31:06 GMT 2025
|
||||
|
||||
adding json would add 76 + 88k to the image, but I think it would also
|
||||
mean we have to rewrite all the default rules in json format
|
||||
|
||||
|
||||
|
||||
with json
|
||||
[dan@loaclhost:~/src/liminix]$ du result/
|
||||
20 result/share/doc/nftables/examples
|
||||
24 result/share/doc/nftables
|
||||
28 result/share/doc
|
||||
12 result/share/man/man3
|
||||
16 result/share/man/man5
|
||||
44 result/share/man/man8
|
||||
76 result/share/man
|
||||
60 result/share/nftables
|
||||
168 result/share
|
||||
36 result/etc/nftables/osf
|
||||
40 result/etc/nftables
|
||||
44 result/etc
|
||||
76 result/bin
|
||||
8 result/include/nftables
|
||||
12 result/include
|
||||
8 result/lib/pkgconfig
|
||||
1172 result/lib
|
||||
1476 result/
|
||||
|
||||
[dan@loaclhost:~/src/liminix]$ du /nix/store/l0zsvldsskiv52b4c9b21ziq5z1qr7vn-jansson-mips-unknown-linux-musl-2.14/
|
||||
84 /nix/store/l0zsvldsskiv52b4c9b21ziq5z1qr7vn-jansson-mips-unknown-linux-musl-2.14/lib
|
||||
88 /nix/store/l0zsvldsskiv52b4c9b21ziq5z1qr7vn-jansson-mips-unknown-linux-musl-2.14/
|
||||
|
||||
without:
|
||||
[dan@loaclhost:~/src/liminix]$ du result/
|
||||
20 result/share/doc/nftables/examples
|
||||
24 result/share/doc/nftables
|
||||
28 result/share/doc
|
||||
12 result/share/man/man3
|
||||
16 result/share/man/man5
|
||||
44 result/share/man/man8
|
||||
76 result/share/man
|
||||
60 result/share/nftables
|
||||
168 result/share
|
||||
36 result/etc/nftables/osf
|
||||
40 result/etc/nftables
|
||||
44 result/etc
|
||||
76 result/bin
|
||||
8 result/include/nftables
|
||||
12 result/include
|
||||
8 result/lib/pkgconfig
|
||||
1096 result/lib
|
||||
1400 result/
|
||||
|
||||
|
||||
Sat Mar 1 23:43:17 GMT 2025
|
||||
|
||||
I don't think json is going to help because either we'd have to do
|
||||
|
||||
elements = map (f: "{{ lookup(f, \"ifname\") }}") zones.${zone}
|
||||
|
||||
and there would be null elements in the places for the interfaces that don't exist
|
||||
yet, or we'd have to write actual json syntax at runtime, at which point why don't
|
||||
we write the trad nftables syntax instead?
|
||||
|
||||
let's write a firewall .nftables file consisting of the zone set
|
||||
elements plus an "include" directive for the rest of the firewall. NOTE THAT
|
||||
we may still need to template the rest of the firewall if we want to have
|
||||
other variables (rate limits) in it, because the rules for that need to be
|
||||
inserted ahead of the rules for accepting icmp, and there's no way to
|
||||
do that without
|
||||
|
@ -18,10 +18,16 @@ in
|
||||
(drop "ip6 saddr 2001:db8::/32") # documentation addresses
|
||||
(drop "ip6 daddr 2001:db8::/32")
|
||||
|
||||
# I think this means "check FIB for (saddr, iif) to see if we
|
||||
# could route a packet to that address using that interface",
|
||||
# and if we can't then it was an inapproppriate source address
|
||||
# for packets received _from_ said interface
|
||||
# Reverse path filtering: drop packet if it's not coming from
|
||||
# the same interface that we'd use to send a reply. Works by
|
||||
# doing a lookup in the FIB to find how we'd route a packet _to_
|
||||
# saddr through iif, and then checking the output interface
|
||||
# returned by the lookup. if oif is 0, that means no route was
|
||||
# found for that address with that interface, so the packet can
|
||||
# be dropped
|
||||
#
|
||||
# https://wiki.nftables.org/wiki-nftables/index.php/Matching_routing_information#fib
|
||||
# https://thr3ads.net/netfilter-buglog/2018/01/2843000-Bug-1220-New-Reverse-path-filtering-using-fib-needs-better-documentation
|
||||
(drop "fib saddr . iif oif eq 0")
|
||||
|
||||
(drop "icmpv6 type router-renumbering")
|
||||
@ -49,8 +55,6 @@ in
|
||||
(drop "ip6 daddr ::FFFF:0:0/96")
|
||||
(drop "ip6 saddr fe80::/10") # link-local unicast
|
||||
(drop "ip6 daddr fe80::/10")
|
||||
(drop "ip6 saddr fc00::/7") # unique-local addresses
|
||||
(drop "ip6 daddr fc00::/7")
|
||||
(drop "ip6 saddr 2001:10::/28") # ORCHID [RFC4843].
|
||||
(drop "ip6 daddr 2001:10::/28")
|
||||
|
||||
@ -196,9 +200,7 @@ in
|
||||
type = "filter";
|
||||
family = "ip";
|
||||
|
||||
rules = [
|
||||
(accept "udp sport 53")
|
||||
];
|
||||
rules = [];
|
||||
};
|
||||
|
||||
input-ip4 = {
|
||||
|
@ -5,6 +5,9 @@
|
||||
nftables,
|
||||
writeFennel,
|
||||
anoia,
|
||||
svc,
|
||||
lua,
|
||||
output-template,
|
||||
lualinux,
|
||||
linotify,
|
||||
}:
|
||||
@ -18,32 +21,61 @@ let
|
||||
inherit (lib.attrsets) mapAttrs' nameValuePair mapAttrsToList;
|
||||
inherit (lib.strings) concatStringsSep;
|
||||
inherit (lib.lists) flatten;
|
||||
inherit (builtins) concatLists toJSON attrValues;
|
||||
inherit (liminix) outputRef;
|
||||
mkSet =
|
||||
family: name:
|
||||
nameValuePair "${name}-set-${family}" {
|
||||
kind = "set";
|
||||
inherit name family;
|
||||
type = "ifname";
|
||||
extraText = ''
|
||||
{{;
|
||||
local services = { ${concatStringsSep ", " (map toJSON zones.${name})} }
|
||||
local ifnames = {}
|
||||
for _, v in ipairs(services) do
|
||||
local o = output(v, "ifname")
|
||||
if o then table.insert(ifnames, o) end
|
||||
end
|
||||
if (#ifnames > 0) then
|
||||
return "elements = { " .. table.concat(ifnames, ", ") .. " }\n"
|
||||
else
|
||||
return ""
|
||||
end
|
||||
}}
|
||||
'';
|
||||
};
|
||||
sets = (mapAttrs' (n: _: mkSet "ip" n) zones) // (mapAttrs' (n: _: mkSet "ip6" n) zones);
|
||||
allRules = lib.recursiveUpdate extraRules (lib.recursiveUpdate (builtins.trace sets sets) rules);
|
||||
sets = (mapAttrs' (n: _: mkSet "ip" n) zones) //
|
||||
(mapAttrs' (n: _: mkSet "ip6" n) zones);
|
||||
allRules = lib.recursiveUpdate extraRules (lib.recursiveUpdate sets rules);
|
||||
script = firewallgen "firewall1.nft" allRules;
|
||||
ifwatch = writeFennel "ifwatch" {
|
||||
packages = [
|
||||
anoia
|
||||
lualinux
|
||||
linotify
|
||||
];
|
||||
mainFunction = "run";
|
||||
} ./ifwatch.fnl;
|
||||
watchArg = z: intfs: map (i: "${z}:${i}/.outputs") intfs;
|
||||
in
|
||||
longrun {
|
||||
name = "firewall";
|
||||
service = longrun {
|
||||
inherit name;
|
||||
run = ''
|
||||
${script}
|
||||
PATH=${nftables}/bin:$PATH
|
||||
${ifwatch} ${concatStringsSep " " (flatten (mapAttrsToList watchArg zones))}
|
||||
PATH=${nftables}/bin:${lua}/bin:$PATH
|
||||
reload() {
|
||||
echo reloading firewall
|
||||
${output-template}/bin/output-template '{{' '}}' < ${script} > /run/${name}/fw.nft;
|
||||
nft -f /run/${name}/fw.nft ;
|
||||
}
|
||||
trap reload SIGUSR1
|
||||
mkdir -p /run/${name}; in_outputs ${name}
|
||||
reload
|
||||
while :; do
|
||||
# signals sent to ash won't interrupt sleep, but will interrupt wait
|
||||
sleep 86400 & wait
|
||||
done
|
||||
'';
|
||||
finish = "${nftables}/bin/nft flush ruleset";
|
||||
};
|
||||
in
|
||||
svc.secrets.subscriber.build {
|
||||
action = "usr1";
|
||||
watch =
|
||||
concatLists
|
||||
(mapAttrsToList (_zone : services : map (s: outputRef s "ifname") services) zones);
|
||||
|
||||
inherit service;
|
||||
}
|
||||
|
@ -13,12 +13,11 @@
|
||||
}:
|
||||
let
|
||||
inherit (liminix.services) oneshot longrun;
|
||||
inherit (builtins) length head toString;
|
||||
inherit (lib) unique optional optionals;
|
||||
inherit (builtins) map length head toString;
|
||||
inherit (lib) unique optional optionals concatStringsSep;
|
||||
inherit (service) name;
|
||||
|
||||
watched-services = unique (map (f: f "service") watch);
|
||||
paths = unique (map (f: f "path") watch);
|
||||
|
||||
restart-flag =
|
||||
{
|
||||
@ -29,23 +28,17 @@ let
|
||||
"quit" = "-s 3";
|
||||
"kill" = "-s 9";
|
||||
"term" = "-s 15";
|
||||
"winch" = "-s 28";
|
||||
"usr1" = "-s 10";
|
||||
"usr2" = "-s 12";
|
||||
"winch" = "-s 20";
|
||||
"usr1" = "-s usr1";
|
||||
"usr2" = "-s usr2";
|
||||
}
|
||||
.${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}";
|
||||
refs = concatStringsSep " "
|
||||
(map (s: "${s "service"}:${s "path"}") watch);
|
||||
in
|
||||
longrun {
|
||||
name = name';
|
||||
@ -55,16 +48,13 @@ let
|
||||
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}
|
||||
${watch-outputs}/bin/watch-outputs ${restart-flag} ${name} ${refs}
|
||||
'';
|
||||
};
|
||||
in
|
||||
service.overrideAttrs (o: {
|
||||
buildInputs = (lim.orEmpty o.buildInputs) ++ optional (watched-service != null) watcher;
|
||||
buildInputs = (lim.orEmpty o.buildInputs) ++ optional (watch != []) watcher;
|
||||
dependencies =
|
||||
(lim.orEmpty o.dependencies)
|
||||
++ optionals (watched-service != null) [
|
||||
watcher
|
||||
watched-service
|
||||
];
|
||||
++ optional (watch != []) watcher;
|
||||
})
|
||||
|
@ -10,6 +10,7 @@ let
|
||||
splitString
|
||||
hasInfix
|
||||
substring
|
||||
optionalString
|
||||
;
|
||||
inherit (lib.lists) groupBy;
|
||||
inherit (lib.attrsets) mapAttrsToList;
|
||||
@ -56,12 +57,14 @@ let
|
||||
name,
|
||||
type,
|
||||
elements ? [ ],
|
||||
extraText ? null,
|
||||
...
|
||||
}:
|
||||
''
|
||||
set ${name} {
|
||||
type ${type}
|
||||
${if elements != [ ] then "elements = { ${concatStringsSep ", " elements} }" else ""}
|
||||
${if elements != [ ] then "elements = { ${concatStringsSep ", " (builtins.trace elements elements)} }" else ""}
|
||||
${optionalString (extraText != null) extraText}
|
||||
}
|
||||
'';
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
check:
|
||||
./output-template '{{' '}}' < example.ini > output
|
||||
diff -u output example.ini.expected
|
@ -2,6 +2,7 @@
|
||||
fetchurl,
|
||||
writeFennel,
|
||||
fennel,
|
||||
fennelrepl,
|
||||
runCommand,
|
||||
lua,
|
||||
anoia,
|
||||
@ -17,9 +18,9 @@ stdenv.mkDerivation {
|
||||
src = ./.;
|
||||
|
||||
buildInputs = [ lua ];
|
||||
doCheck = true;
|
||||
|
||||
nativeBuildInputs = [ fennelrepl ] ;
|
||||
buildPhase = ''
|
||||
fennelrepl --test ./output-template.fnl
|
||||
cp -p ${
|
||||
writeFennel name {
|
||||
packages = [
|
||||
@ -27,11 +28,11 @@ stdenv.mkDerivation {
|
||||
lualinux
|
||||
linotify
|
||||
];
|
||||
macros = [ anoia.dev ];
|
||||
mainFunction = "run";
|
||||
} ./output-template.fnl
|
||||
} ${name}
|
||||
'';
|
||||
checkPhase = "make check";
|
||||
installPhase = ''
|
||||
install -D ${name} $out/bin/${name}
|
||||
'';
|
||||
|
@ -24,21 +24,43 @@
|
||||
(let [delim (.. opening "(.-)" closing)
|
||||
myenv {
|
||||
: string
|
||||
: table
|
||||
: ipairs
|
||||
:output
|
||||
(fn [service-path path]
|
||||
(fn [service-path path default]
|
||||
(let [s (assert (svc.open (.. service-path "/.outputs")))]
|
||||
(s:output path)))
|
||||
:lua_quote #(string.format "%q" %1)
|
||||
(or (s:output path) default)))
|
||||
: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))))))
|
||||
(let [chunk (if (= (x:sub 1 1) ";")
|
||||
(x:sub 2)
|
||||
(.. "return " x))]
|
||||
(assert ((load chunk 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)))
|
||||
|
||||
(import-macros { : define-tests : expect : expect= } :anoia.assert)
|
||||
(define-tests
|
||||
(expect= (pick-values 1 (substitute "var={{ 2 + 3 }}" "{{" "}}")) "var=5")
|
||||
(expect= (pick-values 1 (substitute "{{ json_quote(\"o'reilly\") }}" "{{" "}}"))
|
||||
"\"o'reilly\"")
|
||||
|
||||
(expect= (pick-values 1 (substitute "{{; local a=9; return a }}" "{{" "}}")) "9")
|
||||
|
||||
;; "globals" set in one interpolation are available in subsequent ones
|
||||
(expect= (pick-values 1 (substitute "{{; a=42; return a }} {{ a and 999 or 0 }}" "{{" "}}")) "42 999")
|
||||
|
||||
(fn slurp [name]
|
||||
(with-open [f (assert (io.open name))] (f:read "*a")))
|
||||
(expect=
|
||||
(pick-values 1 (substitute (slurp "example.ini") "{{" "}}"))
|
||||
(slurp "example.ini.expected")))
|
||||
|
||||
{ : run }
|
||||
|
@ -1,3 +0,0 @@
|
||||
check:
|
||||
./output-template '{{' '}}' < example.ini > output
|
||||
diff -u output example.ini.expected
|
@ -2,6 +2,7 @@
|
||||
fetchurl,
|
||||
writeFennel,
|
||||
fennel,
|
||||
fennelrepl,
|
||||
runCommand,
|
||||
lua,
|
||||
anoia,
|
||||
@ -17,9 +18,10 @@ stdenv.mkDerivation {
|
||||
src = ./.;
|
||||
|
||||
buildInputs = [ lua ];
|
||||
# doCheck = true;
|
||||
nativeBuildInputs = [ fennelrepl ] ;
|
||||
|
||||
buildPhase = ''
|
||||
fennelrepl --test ./watch-outputs.fnl
|
||||
cp -p ${
|
||||
writeFennel name {
|
||||
packages = [
|
||||
@ -28,11 +30,12 @@ stdenv.mkDerivation {
|
||||
linotify
|
||||
fennel
|
||||
];
|
||||
macros = [ anoia.dev ];
|
||||
mainFunction = "run";
|
||||
} ./watch-outputs.fnl
|
||||
} ${name}
|
||||
'';
|
||||
# checkPhase = "make check";
|
||||
|
||||
installPhase = ''
|
||||
install -D ${name} $out/bin/${name}
|
||||
'';
|
||||
|
@ -1 +0,0 @@
|
||||
a11
|
@ -1 +0,0 @@
|
||||
a33
|
@ -1 +0,0 @@
|
||||
a55
|
@ -1 +0,0 @@
|
||||
a66
|
@ -1 +0,0 @@
|
||||
000000
|
@ -1 +0,0 @@
|
||||
0000ff
|
@ -1 +0,0 @@
|
||||
00ff00
|
@ -1 +0,0 @@
|
||||
ff0000
|
@ -1 +0,0 @@
|
||||
eth1
|
@ -1,3 +0,0 @@
|
||||
wpa_passphrase={{ output("./example-service","colours/black") }}
|
||||
think = {{ string.format("%q", output("./example-service","colours/blue")) }}
|
||||
argonaut = {{ json_quote "hello\ngoodbye\tnext\027" }}
|
@ -1,3 +0,0 @@
|
||||
wpa_passphrase=000000
|
||||
think = "0000ff"
|
||||
argonaut = "hello\ngoodbye\tnext\u001B"
|
@ -1,44 +0,0 @@
|
||||
(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 }
|
@ -1,10 +1,18 @@
|
||||
(local { : %% : system : assoc : split : table= : dig } (require :anoia))
|
||||
(local svc (require :anoia.svc))
|
||||
(local { : kill } (require :lualinux))
|
||||
(local { : kill &as ll} (require :lualinux))
|
||||
(import-macros { : define-tests : expect : expect= } :anoia.assert)
|
||||
|
||||
(fn split-paths [paths]
|
||||
(icollect [_ path (ipairs paths)]
|
||||
(split "/" path)))
|
||||
(local { : view } (require :fennel))
|
||||
|
||||
(fn output-refs [outputs]
|
||||
(let [result {}]
|
||||
(each [_ v (ipairs outputs)]
|
||||
(let [[service path] (split ":" v)
|
||||
paths (or (. result service) [])]
|
||||
(table.insert paths (split "/" path ))
|
||||
(tset result service paths)))
|
||||
result))
|
||||
|
||||
(fn parse-args [args]
|
||||
(match args
|
||||
@ -17,37 +25,72 @@
|
||||
["-s" signal service & rest] (assoc (parse-args rest)
|
||||
:controlled-service service
|
||||
:action [:signal signal])
|
||||
[watched-service & paths] { : watched-service
|
||||
:paths (split-paths paths)
|
||||
}))
|
||||
outputs { :output-references (output-refs outputs) } ))
|
||||
|
||||
(define-tests
|
||||
(expect= (parse-args ["-r" "daemon"
|
||||
"/nix/store/s1:out1"
|
||||
"/nix/store/s2:out1" "/nix/store/s2:out2/ifname"])
|
||||
{:action "restart"
|
||||
:controlled-service "daemon"
|
||||
:output-references
|
||||
{"/nix/store/s1" [["out1"]]
|
||||
"/nix/store/s2" [["out1"] ["out2" "ifname"]]}}
|
||||
))
|
||||
|
||||
|
||||
(fn changed? [paths old-tree new-tree]
|
||||
(accumulate [changed? false
|
||||
_ path (ipairs paths)]
|
||||
(or changed? (not (table= (dig old-tree path) (dig new-tree path))))))
|
||||
|
||||
(define-tests
|
||||
(expect (changed? [["ifname"]] {:ifname "true"} {:ifindex 2}))
|
||||
(expect (changed? [["ifname"]] {:ifname "true"} {:ifname "false"}))
|
||||
(expect (not (changed? [["ifname"]] {:ifname "true"} {:ifname "true"})))
|
||||
(expect (not (changed? [["mtu"]] {:ifname "true"} {:ifname "false"})))
|
||||
(expect (not (changed? [["mtu"]] {:ifname "true"} {:ifname "false"})))
|
||||
)
|
||||
|
||||
|
||||
|
||||
(fn do-action [action service]
|
||||
(case action
|
||||
:restart (system (%% "s6-svc -r /run/service/%s" service))
|
||||
:restart-all (system (%% "s6-rc -b -d %q; s6-rc-up-tree %q" service service))
|
||||
[:signal n] (system (%% "s6-svc -s %d /run/service/%s" n service))))
|
||||
[:signal n] (system (%% "s6-svc -s %q /run/service/%s" n service))))
|
||||
|
||||
(local POLLIN 0x0001)
|
||||
(local POLLHUP 0x0010)
|
||||
|
||||
(fn wait-for-change [services]
|
||||
(let [pollfds (collect [s _p (ipairs services)]
|
||||
(bor (lshift (s:fileno) 32)
|
||||
(lshift (bor POLLIN POLLHUP) 16)))]
|
||||
(ll.poll pollfds)))
|
||||
|
||||
(fn open-services [output-references]
|
||||
(collect [s p (pairs output-references)]
|
||||
(values (svc.open (.. s "/.outputs")) p)))
|
||||
|
||||
(fn run []
|
||||
(let [{
|
||||
(let [trees {}
|
||||
{
|
||||
: output-references
|
||||
: controlled-service
|
||||
: action
|
||||
: watched-service
|
||||
: paths } (parse-args arg)
|
||||
dir (.. watched-service "/.outputs")
|
||||
service (assert (svc.open dir))]
|
||||
(print (%% "watching %q" watched-service))
|
||||
(accumulate [tree (service:output ".")
|
||||
v (service:events)]
|
||||
: paths } (parse-args arg)]
|
||||
(while true
|
||||
(let [services (open-services output-references)
|
||||
trees (collect [s _ (pairs services)]
|
||||
(values s (s:output ".")))]
|
||||
(wait-for-change services)
|
||||
(each [service paths (pairs services)]
|
||||
(let [new-tree (service:output ".")]
|
||||
(when (changed? paths tree new-tree)
|
||||
(print "watched path event:" action controlled-service)
|
||||
(do-action action controlled-service))
|
||||
new-tree))))
|
||||
(when (changed? paths (. trees service) new-tree)
|
||||
(do-action action controlled-service))))))))
|
||||
|
||||
|
||||
|
||||
{ : run }
|
||||
|
@ -15,6 +15,7 @@ rec {
|
||||
../../modules/ppp
|
||||
../../modules/dnsmasq
|
||||
../../modules/network
|
||||
../../modules/firewall
|
||||
];
|
||||
|
||||
services.pppoe = svc.pppoe.build {
|
||||
@ -23,6 +24,13 @@ rec {
|
||||
password = "NotReallyTheSecret";
|
||||
};
|
||||
|
||||
services.firewall = svc.firewall.build {
|
||||
zones = {
|
||||
wan = [ services.pppoe ];
|
||||
lan = [ services.lan4 ];
|
||||
};
|
||||
};
|
||||
|
||||
services.defaultroute4 = svc.network.route.build {
|
||||
via = "$(output ${services.pppoe} address)";
|
||||
target = "default";
|
||||
@ -39,5 +47,5 @@ rec {
|
||||
domain = "fake.liminix.org";
|
||||
};
|
||||
|
||||
defaultProfile.packages = [ pkgs.hello ];
|
||||
defaultProfile.packages = with pkgs; [ nftables hello ];
|
||||
}
|
||||
|
@ -1,18 +1,46 @@
|
||||
set timeout 60
|
||||
|
||||
spawn socat unix-connect:vm/console -
|
||||
expect "s6-linux-init"
|
||||
send "\r\n"
|
||||
expect "#"
|
||||
send "PS1=\$(echo 'I1JFQURZIyA=' | base64 -d); stty -echo\n"
|
||||
expect "#READY#"
|
||||
set FINISHED 0
|
||||
set EXIT "1"
|
||||
while { $FINISHED < 10 } {
|
||||
send "ip address show dev ppp0 | grep ppp0\r\n"
|
||||
while { $FINISHED < 20 } {
|
||||
send "ip address show dev ppp0 | grep ppp0\n"
|
||||
expect {
|
||||
"192.168.100.1" { set FINISHED 20; set EXIT 0; }
|
||||
"can't find device" { send_user "waiting ..." ; send "\r\n"; sleep 3 }
|
||||
"DOWN" { send_user "waiting ..." ; send "\r\n"; sleep 3 }
|
||||
"192.168.100.1" { set FINISHED 200; set EXIT 0; }
|
||||
"can't find device" { send_user "waiting ..." ; sleep 3 }
|
||||
"DOWN" { send_user "waiting ..." ; sleep 3 }
|
||||
}
|
||||
set FINISHED [ expr $FINISHED + 1 ]
|
||||
}
|
||||
expect "#READY#"
|
||||
send "s6-svwait -U /run/service/wan.link.pppoe\n"
|
||||
expect "#READY#"
|
||||
|
||||
set timeout 30
|
||||
send "nft list set ip table-ip lan || touch /non/existent\n"
|
||||
expect {
|
||||
"lan" { puts "lan found" }
|
||||
"{ }" { puts "missing ifname"; exit 1 }
|
||||
"No such file or directory" { exit 1 }
|
||||
}
|
||||
|
||||
expect "#READY#"
|
||||
|
||||
# if the test fails for no apparent reason, it is quite likely because
|
||||
# the firewall hasn't had time to reload after the new interface
|
||||
# appears and you just have to make this sleep longer. Ew, yes
|
||||
send "sleep 10; nft list set ip table-ip wan || touch /non/existent\n"
|
||||
expect {
|
||||
"ppp0" { puts "ppp0 found " }
|
||||
"{ }" { puts "missing ifname"; exit 1 }
|
||||
"No such file or directory" { exit 1 }
|
||||
timeout { exit 1 }
|
||||
}
|
||||
expect "#READY#"
|
||||
|
||||
exit $EXIT
|
||||
|
Loading…
Reference in New Issue
Block a user